[C#]读书笔记:Effective C#
我看的书是《Effective C#中文版——改善C#程序的50种方法》,Bill Wagner著,李建忠译。书比较老了,04年写的,主要针对C#1.0,但我相信其中的观点现在仍有价值。(平心而论,和Effective C++有差距,毕竟该书成书时对C#的研究不过几年。)
下面是对这本书条款内容的一些归纳和个人理解,由于我比较熟悉C++,因此也会有也一些C++的对比。
第一章 C#语言元素
条款1:使用属性代替可访问的数据成员
1. 属性具有数据成员的访问语法,这是最易于使用的语法。
2. 属性事实上是方法,因而支持多态,且利于日后进行扩展,如多线程同步访问等。
3. .Net中的库功能,很多是针对属性的,例如数据绑定。
4. 两者性能相当。
条款2:运行是常量(readonly)优于编译时常量(const)
1. 两者生成的il码不同,后者是进行常量替换,而前者具有更好的二进制兼容性(程序集B依赖于程序集A的一个常量,如果A中的这个常量修改了,const声明要求AB都重行编译,而readonly只要求A重编)。
2. 两者性能相当。
条款3:操作符is或as优于强制转型
1. as比强制转型具有更简练的语法(等价于as的强制转型,需要包括异常捕获等处理)。
2. 对于ValueType的派生类,由于不能为null,所以不能使用as,应该选择is。
3. 强制转型会考虑用户定义类型转换(explicit;但注意,用户自定义转型是静态的,不支持多态),而as只取决于继承链。
条款4:使用Conditional特性代替#if条件编译
1. 前者能让程序逻辑更清晰,尽管它的最小应用单元是方法而后者是语句。
2. 前者有更好的性能。Conditional是让客户的调用代码消失,而对应功能的条件编译只是让语句消失,函数调用开销依旧。
条款5:总是提供ToString方法
1. 人最容易理解的是字符串,而Object.ToString的默认行为是返回类型名,正确的实现ToString有利于调试和UI等。
2. Console.WriteLine和String.Format等都支持IFormattable接口,注意IFormattable.ToString和Object.ToString的兼容。
条款6:明辨值类型和引用类型的使用场合
1. 需要打包一组数据,且内存占用不多的时候考虑使用struct。
2. 具有数据和逻辑,或者虽然逻辑简单但数据块占内存很大,考虑使用class。
条款7:将值类型尽可能实现为具有常量性和原子性的类型
1. 常量值类型,更容易编写原子逻辑。如,包含姓名和身份证号两个字段的struct,其两个字段是绑定的,单独修改一般会带来错误,所以与其在运行时用方法逻辑来维持两者匹配,不如干脆将两个字段都声明为readonly,如果想修改就用新值来构造新对象。
2. 针对常量对象更容易编写多线程逻辑。
条款8:确保0为值类型的有效状态
1. 因为clr默认将对象初始化为二进制0,所以应该保证二进制0在程序逻辑中是合法值。如枚举值应该从0开始定义。
条款9:理解几个相等判断之间的关系
1. 对于ValueType:(对应struct)
public override bool Equals(object obj);
它的实现主要是通过反射来对比各个字段,因此这个默认实现效率很低。ValueType的默认实现中,并不能直接将两个二进制块进行memcmp,因为形如struct A{ string s; }这样的结构,二进制层次上的对比是没有意义的。事实上,C#编译器也没有提供自动生成T.Equals的服务(即对于用户没有提供Equals实现的struct,编译器何不自动生成逐字段对比的C#代码?),原因不明。
所以,如果特定struct性能攸关,应该手工实现Equals进行逐字段比较以获得更佳性能。另外考虑实现语法糖operator==来调用Equals。
2. 对于Object: (对应class)
public static bool ReferenceEquals(object objA, object objB);
public static bool Equals(object objA, object objB);
public virtual bool Equals(object obj);
public static bool operator == (object objA, object objB);
Object基类中的默认实现全是引用比较,即用于判断是否是同一对象。其中ReferenceEquals提供最底层实现,operator ==调用ReferenceEquals, static Equals进行对象非空验证然后调用virtual Equals, 而virtual Equals默认也是调用ReferenceEquals。
如果需要给引用类型提供其他比较语义,如string,则实现virtual Equals,然后重载operator ==调用virtual Equals。
条款10:理解GetHashCode方法的缺陷。
1. 实现GetHashCode的要求(3点):
正确性要求——
(1)相等的对象必须有相同的hash code,即Equals返回true的对象,GetHashCode返回值也应该相同。值相等的对象当然应该在同一个hash捅中。实现方法:用于生成hash code的字段,一定都要参与Equals的实现。
(2)对象生命期中,GetHashCode返回值应该不变。避免在hash表中查询已经插入的对象却找不到。实现方法:用于生成hash code的字段,最好声明为readonly。
性能要求——
(1)GetHashCode尽量返回均匀分布的值。
2. Object的默认实现是返回自增的全局对象ID,ValueType的默认实现是返回第一个字段的GetHashCode。本条的两点恐怕只适用于C#1.0,我测试在C#3.5中,实现已经变化。
条款11:优先采用foreach循环语句
1. foreach会自动针对不同的容器,生成不同的il码以优化效率。例如对数组,foreach不会通过IEnumerable遍历,而是直接使用下标。
2. foreach可以正确遍历起始下标非0的数组和多维数组。下标非0数组是通过Array.CreateInstance创建的。
3. foreach遍历数组,因为可以保证访问数组的每个元素的时候不越界,故foreach对应的下标访问实现不会有下标越界检查的开销。在我使用的C#3.5中测试,foreach并没有加速效果,恐怕因为在高版本中,下标越界检查已经移到了clr的实现中(il的ldelem),故foreach并不比for循环快。
第二章 .Net资源管理
条款12:变量初始化器优于赋值语句
1. 初始化顺序:(C#3.5,参见step字段)
2 {
3 public string step = "(1)";
4 public InitOrder()
5 {
6 step = "(2)";
7 }
8 }
9 ...
10 InitOrder a = new InitOrder { step = "(3)" };
2. 对象的构造函数可以有多个,比起在多个构造函数中分别初始化字段,字段初始化器更容易维护。
条款13:使用静态构造器初始化静态类成员