visual studio有个功能,代码分析,一般开发完毕后,除了处理常规的“错误列表”显示的“错误”和“警告”,我们更加应该注意的是,运行代码分析功能,规范我们的代码,因为不好的编码习惯,在没有人指出和没有团队氛围的开发中,很多时候都是一路不规范到底
visual studio菜单的“分析”-》“对***运行代码分析”或者在解决方案的类库右击选择代码分析
如果为了强迫自己养成良好的c#微软规范的习惯,我们可以右击类库属性,找到最后一行标签“代码分析”,并在对应右侧明细的“规则集”->"运行此规则集"下拉框中选择Microsoft的所有规则。当然如果你需要每次生成代码时让vs自动帮我们执行代码分析,也可以勾选上复选框“生成时启动代码分析”,在长时间的编码中如果每次都运行代码分析,我们的代码会越来越规范和高效率
我找了以前的很多代码和网上下载的代码,以及公司的一些朋友的代码,逐一代码分析后,总结了如下常规开发中一般会遇到的规范问题,这些都不是错误或者警告,但是对于需要提高自身修养的程序员来说,这是必修课,当然本文只是抛砖引玉,更多的规范在微软的官方文档中都有,只是很多永远不会遇到
MSDN:http://msdn.microsoft.com/zh-cn/library/dd264939(v=vs.100).aspx
下面我将最最最常用的规范问题,总结在一段程序当中(相当简单的程序),朋友们可以不运行代码分析凭借自己的经验来判断,到底有多少处不规范的地方
我敢保证,对于常规的要求不是很严格的开发,以下这些问题或多或少都会在您的代码中出现
调用入口:
1 static void Main(string[] args) 2 { 3 try 4 { 5 Class_Test test1 = new Class_Test(); 6 test1.Fun1(); 7 } 8 catch (Exception ex) 9 { 10 Console.WriteLine(ex.ToString()); 11 } 12 Console.ReadLine(); 13 }
(代码1)
核心代码(为了查看方便把多个类放到同一个文件):
1 namespace TestBLL.Class_Test 2 { 3 public class Class_Test 4 { 5 public void Fun1() 6 { 7 const int param1 = 10; 8 string name = param1.ToString(); 9 if (name == "") 10 { 11 Console.WriteLine("empty"); 12 } 13 else 14 { 15 try 16 { 17 Class_Test2 test2 = new Class_Test2(); 18 test2.Age = 25; 19 Fun_Test1("", ref test2, 100, ""); 20 21 bool param2 = Boolean.Parse(name); 22 Console.WriteLine(param2); 23 List<string> list = new List<string>(); 24 list.Add(name); 25 } 26 catch (Exception ex) 27 { 28 Console.WriteLine(ex.Message); 29 throw ex; 30 } 31 } 32 } 33 public void Fun_Test1(string param1, ref Class_Test2 param2, int param3, string Param4) 34 { 35 string param5 = ""; 36 param2.Age = 24; 37 param2.Fun1(param1, Param4, ref param5); 38 39 } 40 } 41 public class Class_Test2 42 { 43 public int Age { get; set; } 44 public string getTimeType() 45 { 46 string TimeType = string.Empty; 47 int hour = DateTime.Now.Hour; 48 if (hour >= 1 && hour < 5) 49 TimeType = "凌晨"; 50 else if (hour >= 5 && hour < 11) 51 TimeType = "早上"; 52 return TimeType; 53 } 54 public void Fun1(string param, string param1, ref string param2) 55 { 56 # region 57 for (int i = 0; i < 10; i++) 58 { 59 string name = "yhc"; 60 Console.WriteLine(name); 61 } 62 //此处省略99个如上for循环代码块 63 #endregion 64 Fun2(); 65 Console.WriteLine(param); 66 } 67 private void Fun2() 68 { 69 Console.WriteLine(); 70 } 71 } 72 public class Class_Test3 73 { 74 public static void Fun1() 75 { 76 } 77 } 78 public struct StructTest<T> 79 { 80 public List<T> rows; 81 } 82 }
(代码2)
代码分析后有N个警告,大多数都要引起重视
1、CA2210 程序集应具有有效的强名称 用强名称密钥对 'TestBLL.dll' 进行签名。
2、CA1014 用 CLSCompliantAttribute 标记程序集 使用 CLSCompliant(true)来标记 'TestBLL.dll',因为它公开外部可见的类型。
3、 CA1709 标识符的大小写应当正确 更正程序集名称 'TestBLL.dll' 中“BLL”的大小写,将其改为“Bll”。(命名空间、类名等都是如此)
4、CA1707 标识符不应包含下划线 从命名空间名称“TestBLL.Class_Test”中移除下划线。
5、 CA1724 类型名不应与命名空间冲突 类型名 'ClassTest' 与命名空间名称“TestBLL.ClassTest”整体或部分冲突。请更改其中任一名称以消除冲突。
6、CA1305 指定 IFormatProvider 由于 'int.ToString()' 的行为可能会因当前用户的区域设置不同而不同,请将 'ClassTest.Fun1()' 中的此调用替换为对 'int.ToString(IFormatProvider)' 的调用。如果要向用户显示 'int.ToString(IFormatProvider)' 的结果,请指定 'CultureInfo.CurrentCulture' 作为“IFormatProvider”参数。或者,如果软件将存储和访问此结果(例如,当将此结果保留到磁盘或数据库中时),则指定 'CultureInfo.InvariantCulture'。
7、CA1820 使用字符串长度测试是否有空字符串 使用“String.IsNullOrEmpty”调用来替换 'ClassTest.Fun1()' 中的 'string.operator ==(string, string)' 调用。
1 public bool Equals(string value) 2 { 3 if (this == null) 4 { 5 throw new NullReferenceException(); 6 } 7 return value != null && (object.ReferenceEquals(this, value) || (this.Length == value.Length && string.EqualsHelper(this, value))); 8 }
1 public static bool IsNullOrEmpty(string value) 2 { 3 return value == null || value.Length == 0; 4 }
8、CA2200 再次引发以保留堆栈详细信息 'ClassTest.Fun1()' 再次引发捕获的异常并将其显式地指定为一个参数。请改用不带参数的“throw”以保留该异常最初引发时所在的堆栈位置。
C#异常类一、基类Exception C#异常类二、常见的异常类 1、SystemException类:该类是System命名空间中所有其他异常类的基类。(建议:公共语言运行时引发的异常通常用此类) 2、ApplicationException类:该类表示应用程序发生非致命错误时所引发的异常(建议:应用程序自身引发的异常通常用此类) C#异常类三、与参数有关的异常类 此类异常类均派生于SystemException,用于处理给方法成员传递的参数时发生异常 1、ArgumentException类:该类用于处理参数无效的异常,除了继承来的属性名,此类还提供了string类型的属性ParamName表示引发异常的参数名称。 2、FormatException类:该类用于处理参数格式错误的异常。 C#异常类四、与成员访问有关的异常 1、MemberAccessException类:该类用于处理访问类的成员失败时所引发的异常。失败的原因可能的原因是没有足够的访问权限,也可能是要访问的成员根本不存在(类与类之间调用时常用) 2、MemberAccessException类的直接派生类: i、FileAccessException类:该类用于处理访问字段成员失败所引发的异常 ii、MethodAccessException类:该类用于处理访问方法成员失败所引发的异常 iii、MissingMemberException类:该类用于处理成员不存在时所引发的异常 C#异常类五、与数组有关的异常 以下三个类均继承于SystemException类 1、IndexOutOfException类:该类用于处理下标超出了数组长度所引发的异常 2、ArrayTypeMismatchException类:该类用于处理在数组中存储数据类型不正确的元素所引发的异常 3、RankException类:该类用于处理维数错误所引发的异常 C#异常类六、与IO有关的异常 1、IOException类:该类用于处理进行文件输入输出操作时所引发的异常。 2、IOException类的5个直接派生类: i、DirectionNotFoundException类:该类用于处理没有找到指定的目录而引发的异常。 ii、FileNotFoundException类:该类用于处理没有找到文件而引发的异常。 iii、EndOfStreamException类:该类用于处理已经到达流的末尾而还要继续读数据而引发的异常。 iv、FileLoadException类:该类用于处理无法加载文件而引发的异常。 v、PathTooLongException类:该类用于处理由于文件名太长而引发的异常。 C#异常类七、与算术有关的异常 1、ArithmeticException类:该类用于处理与算术有关的异常。 2、ArithmeticException类的派生类: i、DivideByZeroException类:表示整数货十进制运算中试图除以零而引发的异常。 ii、NotFiniteNumberException类:表示浮点数运算中出现无穷打或者非负值时所引发的异常。异常类型
第二:请不要throw ex,让外部代码再去Catch,而应该是throw,再让外部代码再去Catch,其实最好的做法是throw new Exception(ex.Message, ex),以下用ex.ToString()输出异常明细(用于log保存或者异常明细跟踪):
throw ex:抛出新异常,重置堆栈原始异常点,在外部捕捉时根本查看不到具体异常行
System.FormatException: 该字符串未被识别为有效的布尔值。 在 TestBll.ClassTest.Fun1() 位置 e:\测试\console\ConsoleApplication1\TestBLL\Class1.cs:行号 38 在 ConsoleApplication1.Program.Main(String[] args) 位置 e:\测试\console\ConsoleApplication1\ConsoleApplication1\Program.cs:行号 18
throw:抛出异常,外部捕捉时可以看到具体异常行
System.FormatException: 该字符串未被识别为有效的布尔值。 在 System.Boolean.Parse(String value) 在 TestBll.ClassTest.Fun1() 位置 e:\测试\console\ConsoleApplication1\TestBLL\Class1.cs:行号 38 在 ConsoleApplication1.Program.Main(String[] args) 位置 e:\测试\console\ConsoleApplication1\ConsoleApplication1\Program.cs:行号 18
throw new Exception(ex.Message, ex):新建一个异常,然后将ex的堆栈异常信息作为其内部异常(innerException)
System.Exception: 该字符串未被识别为有效的布尔值。 ---> System.FormatException: 该字符串未被识别为有效的布尔值。 在 System.Boolean.Parse(String value) 在 TestBll.ClassTest.Fun1() 位置 e:\测试\console\ConsoleApplication1\TestBLL\Class1.cs:行号 29 --- 内部异常堆栈跟踪的结尾 --- 在 TestBll.ClassTest.Fun1() 位置 e:\测试\console\ConsoleApplication1\TestBLL\Class1.cs:行号 37 在 ConsoleApplication1.Program.Main(String[] args) 位置 e:\测试\console\ConsoleApplication1\ConsoleApplication1\Program.cs:行号 18
仔细观察你会发现,第一种根本找不到具体异常的行,第二种虽然有具体异常信息,但是没有行号,而第三种是最完善的,有具体异常信息,还有具体异常行号
9、CA1801 检查未使用的参数 从未用过 'ClassTest.FunTest1(string, ref ClassTest2, int, string)' 的参数 'param3'。请移除该参数或在方法体中
10、CA1709 标识符的大小写应当正确 在成员 'ClassTest.FunTest1(string, ref ClassTest2, string)' 中,更正参数名称 'Param4' 中“Param”的大小写,将其改为“param”。
11、CA1045 不要通过引用来传递类型 考虑使用不需要将 'param2' 作为canshu.html" target="_blank">引用参数的设计。
1 string param5 = ""; 2 param2 = new ClassTest2(); 3 param2.Age = 24; 4 param2.Fun1(param1, param4, ref param5);
那么是时候考虑用ref了
12、CA1062 验证公共方法的参数 在外部可见方法 'ClassTest.FunTest1(string, ref ClassTest2, string)' 中,请先验证局部变量“'(*param2)'”,然后再使用它,该变量是从参数“param2”重新分配而来的。
13、CA1024 在适用处使用属性 如果可行,请将 'Class_Test2.getTimeType()' 改为属性
CA1822 将成员标记为 static 从未使用 'Class_Test2.getTimeType()' 的“this”参数(Visual Basic 中为“Me”)。根据需要,将成员标记为 static (Visual Basic 中为“Shared”),或者在方法体或至少一个属性访问器中使用“this”/“Me”。
1 public string getTimeType() 2 { 3 string TimeType = string.Empty; 4 int hour = DateTime.Now.Hour; 5 if (hour >= 1 && hour < 5) 6 TimeType = "凌晨"; 7 else if (hour >= 5 && hour < 11) 8 TimeType = "早上"; 9 return TimeType += "好"; 10 }
改为属性的原因:
第一该方法getTimeType修饰符是public,并且没有采用任何参数或返回的值不是数组,这符合属性的要求,同时属性大多数情况下代表的是数据,方法代表的是执行操作,属性在访问更加方便,如果您的所谓的方法逻辑处理不是很多,不是很耗时,并且永远不会因为调用次数的不同而产生不同的结果,那么为什么不优先使用属性呢?
改为静态方法的原因:可以将不访问实例数据或不调用实例方法的成员标记为 static,getTimeType内部没有访问任何其他类实例的属性或者实例方法,也没有访问当前所属类的属性或者字段等数据(没有使用this),那么它就是符合抽象类的条件,这里不讨论静态方法和实例方法性能对比问题,只是代码规范上说这个getTimeType方法不应该定义成实例方法,如果你把这样一个十分常用的实例方法(其他类也会用调用该方法)随便定义在了一个类中,那么你软件的设计将是十分失败,耦合度太高,牵一发而动全身,并且从面向对象的方面来讲,一个对象非常通用的功能为何不定义成对象的基类的方法(如果所有调用类都只是从一个基类继承)或者定义成公共类的静态方法(如果所有调用类南辕北辙)
14、CA1045 不要通过引用来传递类型 考虑使用不需要将 'param2' 作为引用参数的设计。
15、CA1053 静态容器类型不应具有构造函数 由于类型 'Class_Test3' 仅包含“static”成员,因此将它标记为“static”
16、CA1815 重写值类型上的 Equals 和相等运算符 'StructTest1<T>' 应重写相等(==)和不等(!=)运算符。
17、CA1002 不要公开泛型列表 更改 'StructTest1<T>.rows' 中的 'List<T>' 以使用 Collection<T>、ReadOnlyCollection<T> 或 KeyedCollection<K,V> TestBLL Class1.cs
刚开始还不太理解这个设计规范的意思,于是查了下资料,在Why we don’t recommend using List<T> in public APIs 一文中,简要介绍了原因: List<T>类型并不是为可扩展性而设计的,他优化了性能,但是丢失了可扩展性。比如,它没有提供任何可以override的成员。这样就不能获得诸如集合改变时获取通知的功能。Collection<T>集合允许我们重写受保护的SetItem方法,这样当我们向集合中添加或者修改集合中的记录,调用SetItem方法的时候就可以自定义一些事件来通知对象。 List<T>对象有太多的与“场景”不相关的属性和成员,将其作为成员类型对外暴露在一些情况下显得过“重”,比如在WindowsForm中ListView.Items并没有返回一个List对象,而是一个 ListViewItemCollection对象,该对象的签名为: // Summary: // Represents the collection of items in a System.Windows.Forms.ListView control // or assigned to a System.Windows.Forms.ListViewGroup. [ListBindable(false)] public class ListViewItemCollection : IList, ICollection, IEnumerable 是一个实现ICollection接口的对象。 还有在我们常用的DataTable的Rows对象是一个DataRowCollection对象,该对象继承自InternalDataCollectionBase // Summary: //Represents a collection of rows for a System.Data.DataTable. public sealed class DataRowCollection : InternalDataCollectionBase public class InternalDataCollectionBase : ICollection, IEnumerable 而InternalDataCollectionBase则实现ICollection接口。 public class InternalDataCollectionBase : ICollection, IEnumerable 可以看到微软的.NET BCL中没有直接暴露List类型的成员。该规则建议我们使用Collection<T>。 List<T>通常用来作为类的内部实现,因为它对性能进行过优化,具有一些丰富的功能,而Collection<T>则是提供了更多的可扩展性。在编写公共API的时候,我们应该避免接受或者返回List<T>类型的对象,而是使用List的基类或者Collection接口。 Collection<T>虽然可以直接使用,但是通常作为自定义集合的基类来使用,一般的我们应该使用Collection<T>类型的对象来对外暴露功能,除非需要一些List<T>中特有的属性。Collection和List的主要区别和使用场景
18、CA1051 不要声明可见实例字段 由于字段 'StructTest1<T>.rows' 在其声明类型的外部可见,因此,请将它的可访问性改为私有,并添加一个与该字段当前的可访问性相同的属性以提供对该属性的访问。