天天看點

[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字段)

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:使用靜态構造器初始化靜态類成員