我看的書是《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字段)
1 class InitOrder
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:使用靜态構造器初始化靜态類成員