C#概述
C#语法基础
标识符
- 要更注重标识符的清晰而不是简短。
- 不要在标识符名称中使用单词缩写。
- 不要使用不被广泛接受的首字母缩写词,即使被广泛接受,非必要也不要用。
- 要把两个字母的首字母缩写词全部大写,除非它是camelCase标识符的第一个单词。
- 包含三个或更多字母的首字母缩写词,仅第一个字母才要大写, 除非该缩写词是camelCase标识符的第一个单词
- 在camelCase标识符开头的首字母缩写词中,所有字母都不要大写。
- 不要使用匈牙利命名法(不要为变量名称附加类型前缀)。
类型定义
要用名词或名词短语命名类。
要为所有类名使用PascalCase大小写风格。
使用变量
变量的声明
- 要为局部变量使用camelCase风格命名。
注释
- 不要使用注释,除非代码本身“一言难尽”。
- 要尽量写清楚的代码而不是通过注释澄清复杂的算法。
数据类型
基本数值类型
整数类型
- 要在指定数据类型时使用C#关键字而不是BCL名称(例如,使用string而不是String)。
- 要一致而不要变来变去。
字面值
- 要使用大写的字面值后缀(例如1.618033988749895M)
更多基本类型
字符串
- 要依赖S ystem. WriteLine() 和 System.Environment.NewLine 而不是 \n 来确保跨平台兼容。
更多数据类型
可空修饰符
隐式类型的局部变量
- 避免使用隐式类型的局部变量,除非所赋的值的数据类型显而易见。
元组
- 要为元组语法的变量声明使用camelCase大小写规范。
- 考虑为所有元组项名称使用PascalCase大小写风格。
操作符和控制流程
操作符
二元算术操作符(+,-,*,/,%)
- 要用圆括号增加代码的易读性,尤其是在操作符优先级不是让人一目了然的时候。
- 要在字符串可能会本地化时用复合格式化而不是加法操作符来连接字符串。
- 避免在需要准确的十进制小数算术运算时使用二进制浮点类型,改为使用decimal浮点类型。
- 避免将二进制浮点类型用于相等性条件式。要么判断两个值之差是否在容差范围之内,要么使用decimal类型。
递增和递减操作符(++,--)
- 避免递增和递减操作符的使用让人迷惑。
- 在C、C++和C#之间移植使用了递增和递减操作符的代码要小心;C和C++的实现遵循的不一定是和C#相同的规则。
常量表达式和常量符号
- 不要用常量表示将来可能改变的任何值。π和金原子的质子数是常量。金价、公司名和程序版本号则应该是变量。
代码块
- 除非最简单的单行if语句,否则避免省略大括号
布尔表达式
条件操作符(?:)
- 考虑使用if/else语句而不是过于复杂的条件表达式。
控制流程语句(续)
for循环
- 如果被迫要写包含复杂条件和多个循环变量的for循环,考虑重构方法使控制流程更容易理解。
- 事先知道循环次数,且循环中要用到控制循环次数的“计数器”,要使用for循环。
- 事先不知道循环次数而且不需要计数器,要使用while循环。
基本switch语句
- 不要使用continue作为跳转语句退出switch小节。如switch在循环中,这样写合法。但很容易对之后的switch小节中出现的break产生困惑。
跳转语句
goto语句
- 避免使用goto
方法和参数
方法的调用
- 要为方法名使用动词或动词短语。
命名空间
- 要为命名空间使用PascalCase大小写。
- 考虑组织源代码文件目录结构以匹配命名空间层次结构。
方法的声明
参数声明
- 要为参数名使用camelCase大小写。
高级方法参数
参数数组(params)
- 方法能处理任何数量(包括零个)额外实参时要使用参数数组。
可选参数
- 要尽量为所有参数提供好的默认值。
- 要提供简单的方法重载,必须的参数的数量要少。
- 考虑从最简单到最复杂组织重载。
- 要将参数名视为API的一部分;要强调API之间的版本兼容性,就避免改变名称。
用异常实现基本的错误处理
捕捉错误
- 避免从finally块显式抛出异常(因方法调用而隐式抛出的异常可以接受)。
- 要优先使用try/finally而不是try/catch块来实现资源清理代码。
- 要在抛出的异常中描述异常为什么发生。顺带说明如何防范更佳。
- 避免使用常规catch块,用捕捉System.Exception的catch块代替。
- 避免捕捉无法从中完全恢复的异常。这种异常未处理比不正确处理好。
- 避免在重新抛出前捕捉和记录异常。要允许异常逃脱(传播),直至它被正确处理。
使用throw语句报告错误
- 要在捕捉并重新抛出异常时使用空的throw语句,以便保留调用栈。
- 要通过抛出异常而不是返回错误码来报告执行失败。
- 不要让公共成员将异常作为返回值或者out参数。抛出异常来指明错误;不要把它们作为返回值来指明错误。
- 不要用异常处理正常的、预期的情况;用它们处理异常的、非预期的情况。
类
类的声明和实例化
- 不要在一个源代码文件中放多个类。
- 要用所含公共类型的名称命名源代码文件。
属性
属性和字段的设计规范
要使用属性简化对简单数据的访问(只进行简单计算)。
避免从属性取值方法抛出异常。
要在属性抛出异常时保留原始属性值。
-
如果不需要额外逻辑,要优先使用自动实现的属性,而不是属性加简单支持字段。
考虑为支持字段和属性使用相同的大小写风格,为支持字段附加“_”前缀。但不要使用双下划线,它是为 C# 编译器保留的。
要使用名词、名词短语或形容词命名属性。
考虑让某个属性和它的类型同名。
避免用 camelCase 大小写风格命名字段。
如果有意义的话,要为 Boolean 属性附加 ”Is“ “Can” 或 “Has” 前缀。
不要声明 public 或 protected 实例字段(而是通过属性公开)。
要用 PascalCase 大小写风格命名属性。
要优先使用自动实现的属性而不是字段。
如果没有额外的实现逻辑,要优先使用自动实现的属性而不是自己写完整版本。
提供属性验证
- 避免从属性外部(即使是从属性所在的类中)访问属性的支持字段。
- 创建 ArgumentException() 或 ArgumentNullException() 类型的异常时,要为 paramName 参数传递 “value” ,它是属性赋值方法隐含的参数名。
只读和只写属性
- 如属性值不变,要创建只读属性。
- 如属性值不变,从 C# 6.0 起要创建只读自动实现的属性而不是只读属性加支持字段。
取值和赋值方法的访问修饰符
- 要为所有属性的取值和赋值方法应用适当的可访问性修饰符。
- 不要提供只写属性,也不要让赋值方法的可访问性比取值方法更宽松。
构造函数
对象初始化器
- 要为所有属性提供有意义的默认值,确保默认值不会造成安全漏洞或造成代码执行效率大幅降低。自动实现的属性通过构造函数设置默认值。
- 要允许属性以任意顺序设置,即使这会造成对象短时处于无效状态。
重载构造函数
- 如构造函数的参数只是用于设置属性,构造函数参数 (camelCase) 要使用和属性 (PascalCase) 相同的名称,区别仅仅是首字母的大小写。
- 要为构造函数提供可选参数,并且/或者提供便利的重载构造函数,用好的默认值初始化属性。
静态成员
静态构造函数
- 考虑要么以内联方式初始化静态字段(而不要使用静态构造函数),要么在声明时初始化。
扩展方法
- 避免随便定义扩展方法,尤其是不要为自己无所有权的类型定义。
封装数据
const
- 要为永远不变的值使用常量字段。
- 不要为将来会发生变化的值使用常量字段。
readonly
- 从 C# 6.0 开始,要优先选择只读自动实现的属性而不是只读字段。
- 从 C# 6.0 之前,要为预定义对象实例使用 public static readonly 字段。
- 如要求版本API兼容性,在 C# 6.0 或更高版本中,避免将 C# 6.0 之前的 public readonly 字段修改成只读自动实现属性。
嵌套类
- 避免声明公共嵌套类型。少数高级自定义场景才需考虑。
接口
接口概述
- 接口名称要使用 Pascal 大小写,加 “I” 前缀。
接口实现
显式和隐式接口实现的比较
- 避免显式实现接口成员,除非有很好的理由。但如果不确定,优先显式。
通过接口实现多继承
- 考虑定义接口获得和多继承相似的效果。
版本控制
- 不要为已交付的接口添加成员。
比较接口和类
- 一般优先选择类而不是接口。用抽象类分离契约(类型做什么)与实现细节(类型怎么做)。
- 如果需要使已从其他类型派生的类型支持接口定义的功能, 考虑 定义接口。
比较接口和特性
- 避免使用无成员的标记接口,改为使用特性。
值类型
- 不要创建消耗内存大于16字节的值类型
结构
- 要创建不可变的值类型。
初始化结构
- 要确保结构的默认值有效;总是可以获得结构的默认“全零”值。
值类型的继承和接口
- 如需比较相等性,要在值类型上重写相等性操作符 (Equals(),== 和 !=) 并考虑实现 IEquatable
接口。
装箱
- 避免可变值类型。
枚举
- 考虑使用默认 32 位整型作为枚举基础类型。只有出于互操作性或性能方面的考虑才使用较小的类型,只有创建标志 (flag) 数超过 32 个的标志枚举才使用较大的类型。
- 考虑在现有枚举中添加新成员,但是要注意兼容性风险。
- 避免创建代表“不完整”值(如版本号)集合的枚举。
- 避免在枚举中创建 “保留给将来使用” 的值。
- 避免包含单个值的枚举。
- 要为简单枚举提供值 0 来代表无。注意若不显示初始化, 0 就是默认值。
在枚举和字符串之间转换
- 如果字符串必须本地化成用户语言,避免枚举/字符串直接转换。
枚举作为标志使用
- 要用 FlagsAttribute 标记包含标志的枚举。
- 要为所有标志枚举提供等于 0 的 None 值。
- 避免将标志枚举中的零值设定为 “所有标志都未设置” 之外的其他意思。
- 考虑为常用标志组合提供特殊值。
- 不要包含 “哨兵” 值(如 Maximum ),这种值会使用户困惑。
- 要用 2 的乘方确保所有标志组合都不重复。
小结
- 不要定义结构,除非它逻辑上代表单个值,消耗 16 字节或更少存储空间,不可变,而且很少装箱。
合式类型
重写 object 的成员
重写 ToString()
- 如需返回有用的、面向开发人员的诊断字符串,就要重写 ToString() 。
- 要使 ToString() 返回的字符串简短。
- 不要从 ToString() 返回空字符串来代表 “空” (null) 。
- 避免 ToString() 引发异常或造成可观察到的副作用(改变对象状态)。
- 如果返回值与语言文化相关或要求格式化(例如 DateTime ),就要重载 ToString(string format) 或实现 IFormattable 。
- 考虑从 ToString() 返回独一无二的字符串以标识对象实例。
重写 Equals()
- 要一定 GetHashCode() 、 Equals() 、 == 操作符和 != 操作符,缺一不可。
- 要用相同算法实现 Equals() 、 == 和 != 。
- 避免在 GetHashCode() 、 Equals() 、 == 和 != 的实现中引发异常。
- 避免在可变引用类型上重载相等性操作符(如重载的实现速度过慢,也不要重载)。
- 要在实现 IComparable 时实现与相等性相关的所有方法。
操作符重载
比较操作符(==,!=,<,>,<=,>=)
- 避免在 == 操作符的重载实现中使用该操作符。
转换操作符
转换操作符规范
- 不要为有损转换提供隐式转换操作符。
- 不要从隐式转换中引发异常。
定义命名空间
- 要为命名空间附加公司名前缀,防止不同公司使用同一个名称。
- 要为命名空间二级名称使用稳定的、不随版本升级而变化的产品名称。
- 不要定义没有明确放到一个命名空间中的类型。
- 考虑创建和命名空间层次结构匹配的文件夹结构。
XML 注释
生成 XML 文档文件
- 如果签名不能完全说明问题,要为公共 API 提供 XML 文档,其中包括成员说明、参数说明和 API 调用示例。
资源清理
垃圾回收、终结和 IDisposable
要只为使用了稀缺或昂贵资源的对象实现终结器方法,即使终结会推迟垃圾回收。
要为有终结器的类实现 IDisposable 接口以支持确定性终结。
要为实现了 IDisposable 的类实现终结器方法,以防 Dispose() 没有被显示调用。
要重构终结器方法来调用与 IDisposable 相同的代码,可能就是调用一下 Dispose() 方法。
不要在终结器方法中抛出异常。
要从 Dipose() 中调用 System.GC.SuppressFinalize() ,以使垃圾回收更快地发生,并避免重复性的资源清理。
要保证 Dispose() 可以重入(可被多次调用)
要保持 Dispose() 的简单性,把重点放在终结所要求的资源清理上。
避免为自己拥有的、带终结器的对象调用 Dispose() 。相反,依赖终结队列清理实例。
避免在终结方法中引用未被终结的其他对象。
要在重写 Dispose() 时调用基类的实现。
考虑在调用 Dispose() 后将对象状态设为不可用。对象被 dispose 之后,调用除 Dispose() 之外的方法应引发 ObjectDisposedException 异常。( Dispose() 应该能多次调用。)
要为含有可 dispose 字段(或属性)的类型实现 IDisposable 接口,并 dispose 这些字段所引用的对象。
异常处理
多异常类型
- 要在向成员传递了错误参数时抛出 ArgumentException 或者它的某个子类型。抛出可能具体的异常(例如 ArgumentNullException )。
- 要在抛出 ArgumentException 或者它的某个子类时设置 ParamName 属性。
- 要抛出最能说明问题的异常(最具体或者派生得最远的异常)。
- 不要抛出 NullReferenceException 。相反,在值意外为空时抛出 ArgumentNullException 。
- 不要抛出 System.SystemException 或者它的派生类型。
- 不要抛出 System.Exception 或者 System.ApplicationException 。
- 考虑在程序继续执行会变得不安全时调用 System.Environment.FailFast() 来终止进程。
- 要为传给参数异常类型的 paramName 实参使用 nameof 操作符。接收这种实参的异常包括 ArgumentException ,ArgumentOutOfRangeException 和 ArgumentNullException 。
异常处理规范
- 避免在调用栈比较低的位置报告或记录异常。
- 不该捕捉的异常不要捕捉。要允许异常在调用栈中向上传播,除非能通过程序准确处理栈中较低位置的错误。
- 如理解特定异常在给定上下文中为何发生,并能通过程序处理错误,就考虑捕捉该异常。
- 避免捕捉 System.Exception 或 System.SystemException ,除非是在顶层异常处理程序中先执行最终的清理操作再重新抛出异常。
- 要在 catch 块中使用 throw ; 而不是 throw <异常对象> 语句。
- 要先想好异常条件,避免在 catch 块重新抛出异常。
- 重新抛出不同异常时要当心。
- 值意外为空时不要抛出 NullReferenceException ,而应抛出 ArgumentNullException 。
- 避免在异常条件表达式中抛出异常。
- 避免以后可能变化的异常条件表达式。
自定义异常
- 如果异常不以有别于现有 CLR 异常的方式处理,就不要创建新异常。相反,应抛出现有的框架异常。
- 要创建新异常类型来描述特别的程序错误。这种错误无法以现有的 CLR 异常来描述,而且需要采取有别于现有 CLR 异常的方式以程序化的方式处理。
- 要为异常类的名称附加 “Exception” 后缀。
- 要使异常能由 “运行时” 序列化。
- 考虑提供异常属性,以便通过程序访问关于异常的额外信息。
- 避免异常继承层次结构过深。
重新抛出包装的异常
- 如果低层抛出的特定异常在高层运行的上下文中没有意义, 考虑 将低层异常包装到更恰当的异常中。
- 要在包装异常时设置内部异常属性。
- 要将开发人员作为异常的接收者,尽量说清除问题和解决问题的办法。
泛型
泛型类型概述
类型参数命名规范
- 要为类型参数选择有意义的名称,并为名称附加 “T” 前缀。
- 考虑在类型参数的名称中指明约束。
泛型的接口和结构
- 避免在类型中实现同一泛型接口的多个构造。
多个类型参数
- 要将只是类型参数数量不同的多个泛型类放到同一个文件中。
嵌套泛型类型
- 避免在嵌套类型中用同名参数隐藏外层类型的类型参数。
协变性和逆变性
数组对不安全协变性的支持
- 避免不安全的数组协变。相反,考虑将数组转换成只读接口 IEnumerable
,以便通过协变转换来安全地转换。
委托和 Lambda 表达式
声明委托类型
常规用途的委托类型: System.Func 和 System.Action
- 考虑定义自己的委托类型对于可读性的提升,是否比使用预定义泛型委托类型所带来的便利性来得更重要。
Lambda 表达式
语句 Lambda
- 如类型对于读者显而易见,或者是无关紧要的细节,就考虑在 Lambda 形参列表中省略类型。
匿名方法
- 避免在新代码中使用匿名方法语法,优先使用更简洁的 Lambda 表达式语法。
外部变量
- 避免在匿名函数中捕捉循环变量。
事件
使用多播委托编码 Publish-Subscribe 模式
检查空值
- 要在调用委托前检查它的值是不是空值。
- 要从 C# 6.0 起在调用 Invoke() 前使用空条件操作符。
理解事件
编码规范
- 要在调用委托前验证它的值不为 null 。( C# 6.0 起应使用空条件操作符。)
- 不要 为非静态事件的 sender 传递 null 值,但要为静态事件的 sender 传递 null 值。
- 不要 为 eventArgs 传递 null 值。
- 要 为事件使用 EventHandler
委托类型。 - 要为 TEventArgs 使用 System.EventArgs 类型或者它的派生类型。
- 考虑使用 System.EventArgs 的子类作为事件参数类型( TEventArgs ),除非确定事件永远不需要携带任何数据。
泛型和委托
- 要为事件处理程序使用 System.EventHandler
而非手动创建新的委托类型,除非必须用自定义类型的参数名加以澄清。
支持标准查询操作符的集合接口
标准查询操作符
使用 Count() 对元素进行计数
- 要在检查是否有项时使用 System.Linq.Enumerable.Any() 而不是调用 Count() 方法。
- 要使用集合的 Count 属性(如果有的话),而不是调用 System.Linq.Enumerable.Count() 方法。
使用 OrderBy() 和 ThenBy 来排序
- 不要为 OrderBy() 的结果再次调用 OrderBy() 。附加的筛选依据用 ThenBy() 指定。
使用查询表达式的 LINQ
查询表达式只是方法调用
- 要用查询表达式使查询更易读,尤其是涉及复杂的 from , let , join 或 group 子句时。
- 考虑在查询所涉及的操作没有对应的查询表达式时使用标准查询操作符(方法调用),例如 Count() , TakeWhile() 或者 Distinct() 。
多线程处理
多线程处理基础
- 不要以为多线程必然会使代码更快。
- 要在通过多线程来加快几角处理器受限问题时谨慎衡量性能。
- 不要无根据地以为普通代码中的原子性操作在多线程代码中也是原子性的,
- 不要以为所有线程看到的都是一致的共享内存。
- 要确保同时拥有多个锁的代码总是以相同的顺序获取它们。
- 避免所有竞态条件条件,程序行为不能受操作系统调度线程的方式的影响。
使用 System.Threading
生产代码不要让线程进入睡眠
- 避免在生产代码中调用 Thread.Sleep() 。
生产代码不要中断线程
- 避免在生产代码中中断 (abort) 线程,因为可能发生不可预测的结果,使程序不稳定。
线程池处理
- 要用线程池向处理器受限任务高效地分配处理器时间。
- 避免将池中的工作者线程分配给 I/O 受限或长时间运行的任务。改为使用 TPL 。
异步任务
用 AggregateException 处理 Task 上的未处理异常
- 避免程序在任何线程上产生未处理异常。
- 考虑登记“未处理异常”事件处理程序以进行调试、记录和紧急关闭。
- 要取消未完成的任务而不是在程序关闭期间允许其运行。
取消任务
长时间运行的任务
- 要告诉任务工厂新建的任务可能长时间运行,使其能恰当地管理它。
- 要少用 TaskCreationOptions.LongRunning 。
线程同步
线程同步的意义
为什么要避免锁定this、typeof(type)和string
- 避免锁定 this、typeof(type) 或字符串
- 要为同步目标声明 object 类型的一个单独的只读同步变量。
- 避免用 MethodImplAttribute 同步
同步设计最佳实践
- 不要以不同顺序请求相同两个或更多同步目标的排他所有权。
- 要确保同时持有多个锁的代码总是相同顺序获得这些锁。
- 要将可变的静态数据封装到具有同步逻辑的公共 API 中。
- 避免同步对不大于本机(指针大小)整数的值的简单读写操作,这种操作本来就是原子性的。
平台互操作性与不安全代码
平台调用
错误处理
- 如果非托管方法使用了托管代码的约定,比如结构化异常处理,就要围绕非托管方法创建公共托管包装器。
设计规范
- 不要无谓重复现有的、已经能执行的非托管 API 功能的托管类。
- 要将外部方法声明为私有或内部。
- 要提供使用了托管约定的公共包装器方法,包括结构化异常处理、为特殊值使用枚举等。
- 要为非必须参数选择默认值来简化包装器方法。
- 要用 SetLastErrorAttribute 将使用 SetLastError 错误码的 API 转换成抛出 Win32Exception 的方法。
- 要扩展 SafeHandle 或实现 IDisposable 并创建终结器来确保非托管资源被高效清理。
- 要在非托管 API 需要函数指针的时候使用和所需方法的签名匹配的委托类型。
- 要尽量使用 ref 参数而不是指针类型。