阅读《Effective C#》一书的总结,第一部分,C#语言习惯。
C# Language Idioms
01 使用属性Property而不是直接访问成员数据
使用Property可支持Data binding;
使用Property更容易应对需求改变引起的实现的变化;
Property内部是基于Method,所以可以很容易支持多线程(set、get时加锁):
1 | public class Customer |
Property可以定义为抽象的,也可以在接口中定义;
Property可是实现超出成员变量以外的功能,如:
1 | // 直接索引 |
02 使用readonly而不是const
readonly
是运行时常量,const
是编译时常量;
编译时常量速度更快,但务必保证不同版本的发布中其值是不变的。运行时常量速度稍慢,但是更灵活;
const
仅适用于数值和字符串,readonly
可用于带有构造函数的类型;
03 使用is或as操作符而不是类型转换
使用as
,更安全,运行时更高效;
as
和is
不会执行任何用户定义的类型转换操作符,即只有运行时类型匹配才会成功;
as
更简洁,更易读,且避免了使用try ... catch ...
;
as
无法处理值类型,可以先用is
判断再cast
;
04 使用Conditional属性而不是#if
使用#if/#endif
使代码难以阅读和调试;
Conditional
属性仅适用于方法,且方法返回类型为void
;
Conditional
属性不会影响其修饰的函数生成的代码,修改的是对函数的调用;
当函数有多个Conditional
属性时,它们是“或”的关系;
05 总是提供ToString()
可以便捷地向使用者展示对象的信息;
对于更复杂的对象,实现IFormattable.ToString()
;
06 理解多种“相等”概念的关系
C#中当定义了类或者结构时,需要为这个类型定义何谓“相等”,C#中有下边四种相等的概念:
1 | // 方法1 |
通常前二者不应当重定义;
方法1ReferenceEquals
用于比较二者是否是同一引用,如果传入参数为值类型,则会自动装箱;
以下代码总是输出”Always happens.”:
1 | int i = 5; |
方法2Object.Equals()
内部会用到方法1和方法3,其实现类似于下边的逻辑:
1 | public static new bool Equals(object left, object right) |
对于方法3,实例方法Equals()
,需要实现IEquatable<T>
来覆写;
值类型(结构体),应该实现Equals()
方法。否则在比较时会调用默认的比较方法(基于反射的逐成员对比,很低效);
引用类型在调用Equals()
时默认会比较引用,如希望通过比较内容来判定,则覆写Equals()
;
值类型应当覆写operator ==
,以避免低效的反射及不必要的装箱拆箱;
引用类型不要覆写operator ==
,因为需要保持其比较引用的原始含义;
IStructuralEquality
适用于System.Array
和Tuple<>
,使其使用值类型语义;
07 理解GetHashCode()的缺陷
应避免使用,仅用在基于hash的集合中为键定义hash值,如HashSet
对于引用类型,可以取到正确的结果但是很低效,对于值类型数据取到的是不正确的结果;
覆写GetHashCode()
务必满足三个条件:
相等的对象(
operator==
)得到相等的hash值;对于一个实例,hash值应保持固定不变;
函数得到的hash值应当在所有的整数中随机分布;
08 使用查询语法而不是循环
使用查询语法使代码逻辑由可读性差的命令式(imperative)变成易于理解的陈述式(declarative);
查询语法和循环,二者的效率孰高孰低需要根据不同的情形而定;
一些使用查询语法的例子:
1 | int[] foo = (from n in Enumerable.Range(0, 100) |
1 | private static IEnumerable<Tuple<int, int>> QueryIndices() |
09 在API中避免类型转换操作
使用类型转换操作可以实现不同的类的可替换性(substitutability),例如:
1 | static public implicit operator Ellipse(Circle c) |
注意这样的代码会有很大的隐患,当出现以下的场景时:
1 | public static void Flatten(Ellipse e) |
Flatten()
会作用于一个临时对象,然后该对象会变成没有引用的垃圾。函数的调用并不会产生实际的效果。正确的方式是使用构造函数来代替类型转换:
1 | Circle c = new Circle(new PointF(3.0f, 0), 5.0f); |
10 使用可选参数来减少方法重载
使用具名参数和可选参数,会使代码逻辑更清晰;
具名参数的一个例子:
1 | private void SetName(string lastName, string firstName) |
在维护程序的过程中,对于public或protected接口,尽量避免修改参数及参数名,而是提供重载方法。参数名也是接口的一部分;
11 理解小函数的魅力
C#代码到可执行的机器码需要两步,首先C#编译器将代码生成IL代码存在程序集中,JIT按需将各方法生成机器码;
小函数可以让JIT编译器更方便地按需编译,而不必一次性编译大量的不会立即使用的代码;
小函数跟利于JIT编译器的寄存器化(enregistration),即将一些局部变量放在寄存器而不是栈上。局部变量的数量越少越好;
小函数更容易被内联;
REFERENCE
《Effective C#: 50 Specific Ways to Improve Your C# (2nd Edition)》