Effective C#筆記(1)

(1)  使用Property的效率問題

其實使用Property的效率並不會很差,C#編譯器會把一些Property編譯成inline的方式,這樣和Variable的效率是一樣的。即使沒有被編譯成inline,其效率也只是比Variable差一點,並且沒有到足於需要我們考慮的時候

(2) 先使用Variable,必要的時候再轉成Property

這樣會引起Binary Compatible問題,就是DLL兼容性問題了。因爲Variable和Property編譯生成的IL是不同的,這樣如果以後更新的時候,必須把所有關連的DLL都編譯一遍,否則會造成DLL之間的不兼容問題。

(3) 爲什麼建議使用ReadOnly而不是Const

因爲Const在編譯的時候就已經替換到變量中去了,如果以後某個DLL中的Const更新,而沒有重新編譯使用到這個Const的DLL,就會造成不一致性(看來是需要很重視DLL之間的兼容性問題)
比如,DLLA中定義了Const ConstValue = 100,DLLB中使用了這個ConstValue值。在DLL更新過程中,把DLLA裏面的ConstValue值更新爲200,而這時候如果沒有重新編譯DLLB,則DLLB使用的ConstValue值仍然是沒有更新的100,而不是200。

(4) C#編譯器根據編譯時的類型而不是運行時的類型來生成代碼

其實這個很明顯,編譯器當然不可能根據運行時來生成代碼了:) 但遇到一些需要應用這個邏輯的時候,卻經常想不起來。
比如Class A中定義了MyType這種Operator
public static implicit operator MyType(A t)
在使用過程中,我們可能會用到
Object obj = (create a new object typeof A)
MyType t = (MyType)obj;
我們會認爲以上的Cast執行的是A自定義的MyType方法,其實編譯器在編譯的時候只知道obj的類型是Object,而不知道其運行時的類型是A,因此在編譯的時候就已經生成了這條語句的執行IL,就是使用Object的Cast方法,而不是A的MyType方法。

(5) foreach會產生CastException

其實也很容易,因爲foreach要對Value Type和Class Type都有效,而as對於Value Type是不起作用的,所以foreach會生成類似以下的語句
IEnumerator it = collection.GetEnumerator();
While (it.MoveNext())
{
       CustomType t = (CustomType)it.Current;
       ....
}
因爲有Cast,所以就有可能產生CastException.
那至於爲什麼Value Type不能使用as操作符呢?試想如果int a = o as int; 如果o不是int,那就應該返回null值,但int並沒有null值,所以也就不能使用as操作符了。

(6) 使用了as,就不需要使用is了

因爲如果使用了as的時候還使用is就會寫出多餘的代碼
比如
Object o = (Create New Object A);
A a = null;
if (o is A)
{a = o as A;}
完全可以用A a = o as A來代替。

(7) ToString方法

如果對一個類實現了IFormattable.ToString(format, formatProvider)接口,則必須保證"G",""和null format能夠返回Object.ToString()一樣的結果。因爲.NET FCL使用IFormattable.ToString如果一個類實現了這個接口,而不是使用Object.ToString(),FLC經常調用IFormattable.ToString,傳入null參數,而一些地方則使用"G"去表示返回一般的格式。如果這些結果不一致,會破壞一些轉換的規則。

另外,對於用戶定義的IFormatProvider和ICustomFormatter,調用的順序如下(比如語句Console.WriteLine(string.Format(new CustomFormatterProvider(), "{0}", value)):
a. 某個ICustomFormatter實例(注:爲什麼說某個,是因爲我不知道,應該是FCL裏面的某個實例)會調用CustomFormatterProvider.GetFormat取得ICustomFormatter實例,也就是說CustomFormatterProvider.GetFormat的實現裏面必須判斷傳入的Type是不是爲ICustomFormatter
public object GetFormat(Type formatType)
{
     if (formatType == typeof(ICustomFromatter) {return ...}
}
b.如果以上的方法返回null的ICustomFormatter實例,則如果類實現了IFormattable接口,則會調用IFormattable.ToString(format, formatProvider)方法
c. 否則如果類沒有實現IFormattable接口,則會調用value類本身的Object.ToString()方法。

(8) 值類型和引用類型

內存分配上的差別:值類型在聲明的時候已經分配好內存了,並且值類型是在棧上分配內存的。引用類型在聲明的時候並沒有分配好內存,是分配在堆上的
比如:MyType [] var = new MyType[101],如果MyType是值類型,則會在棧上分配100個連續的空間來存儲100個MyType值;而如果MyType是引用類型,則會首先在棧上分配100個連續的空間來存儲100個"指針",這些"指針"指向的值是未定的(相當於null值),當使用MyType[i] = new MyType()的時候,纔在堆上分配一個空間給MyType值,並把第i個指針指向新申請的空間。

至於爲什麼值類型分配在棧上,而引用類型分配在堆上?我個人認爲因爲值類型的有效範圍肯定在聲明之後才使用,直至棧上要刪除這個值的時候,該值類型的作用域已經完成了;而引用類型因爲要在程序裏面引用多次,比如在一個函數裏面聲明的變量可能會通過返回值給其它函數使用,其空間不能隨着函數作用域的結束而釋放,所以需要存儲在堆上。

值類型在空間使用效率上比引用類型高:連續分配;在棧上分配減少垃圾;調用的時候可以直接找到,而不需要通過"指針"來轉向。引用類型能夠支持多態,能夠支持子類,代表了對象的行爲,這些是值類型所沒有的。

(9) 不可變的原子值類型

不可變的原子值類型能夠很好地支持多線程,支持基於Hash的集合。定義不可變的原子值類型可以考慮以下幾點:(a)提供不同的構造函數,使得更容易使用;(b)使用工廠類的方法創建值類型,比如Color就提供了Color.FromKnownColor和Color.FormName來創建Color類;(c)提供可變的類,比如StringBuilder是String的可變類型,通過可變的類來創建好值後再轉換成不可變的原子類。

(10) 對於值類型,0值要有意義

值類型的0值要有意義。其實我覺得這一條主要是針對Enum類型來說的,我們經常會設計如下的Enum類型 enum MyEnum { A=1, B=2, C=3},但對於有些用戶,可能會通過MyEnum var = new MyEnum() 來聲明Enum值,這時候就使得var處於無效和狀態下。

(11) Equals方法

對於Equals方法,C#提供了四種不同的比較方法:
public static bool ReferenceEquals(object left, object right)
public static bool Equals(object left, object right)
public virtual bool Equals(object right)
public static bool operator==(MyClass left, MyClass right)
以下對這四種方法進行分析
(a) ReferenceEquals函數,不能重寫這種函數。這個函數比較兩個對象的Identity是否相等,對於引用類型的對象來說,如果兩個對象指向同一個對象,則這個函數返回True;而對於值類型對象,這個函數永遠返回False。比如int i = 5; Object.ReferenceEquals(i, i) = false.這個是比較奇怪的地方。
(b) static Equals函數,不能重寫這個函數。這個函數的實現如下所示
if (left == right) return true;
if (left == null || right == null) return false;
return left.Equals(right);
所以這個函數依賴於下面要提到的Equals方法的實現。
(c) Equals函數。對於引用類型來說,如果沒有重寫這個函數,則其比較方法和ReferenceEquals函數相同。對於值類型對象,值類型的基類ValueType已經重寫了這個函數,只要值類型的所有的內容都相同,則這個函數返回True。但對於值類型對象有個效率的問題,因爲ValueType要對所有繼承其的值類型都要適用,所以只能使用反射來取得所有的屬性,衆所周知,反射的效率是非常非常慢的。因此,對於所有的值類型對象,都要重寫這個函數。而對於引用類型的對象,如果需要在語義的層次定義其相等性,則可以重寫這個函數。
對於這個函數的實現,有一般的實現方式
public override bool Equals(object right)
{
    if (right == null) return false;
    if (Object.ReferenceEquals(this, right)) return true;
    if (this.GetType() != right.GetType()) return false;
    // Compare other members.
}
至於爲什麼要用this.GetType來比較,而不用LeftClass rightAsLeftClass = right as LeftClass,再判斷rightAsLeftClass是否爲Null的方式來比較?因爲如果我們採用這種方式來比較,則left as RightClass和right as LeftClass中有可能會有一個永遠返回Null(很明顯,子類可以轉換成基類,基類不能轉換成子類),則這時候會違反傳遞性的原則,即left.Equals(right)可能返回True,而right.Equals(left)永遠返回False。
另外,對於Equals方法是不能拋異常的。在子類的Equals方法中,一般要調用基類的Equals方法,除非基類是Object類型。
(d) ==函數,對於值類型,要重寫這個函數,其原因和Equals函數的原因是一樣的,都是爲了效率。而對於引用類型的對象,則不建議重寫這個函數,因爲默認的行爲就是要判斷引用是否指向同一個對象。

(12) GetHashCode方法

這個函數只用於一個地方:用來定義基於哈希的集合的鍵值,比如Hashtable和Dictionary。所以如果你的類不是用在哈希的Key中,則不需要考慮這個方法。
對於所有重寫這個該當的函數,必須滿足以下三個條件:
(a) 如果兩個對象是相等的(通過==比較,不是Equals函數),則這個函數必須返回相同的值。
(b)對於一個類A的對象,其GetHashCode方法必須永遠返回相同的值,不管如何操作這個對象,如何調用這個對象上的函數。這是爲了保證一個對象存儲進哈希集合後,能夠被正確讀取出來。
(c)這個函數對於所有的輸入要生成隨機均勻的分佈。
首先來看看Object.GetHashCode和ValueType.GetHashCode的實現方法有什麼特別的地方。
對於Object.GetHashCode,這個方法是正確的,但其效率不好(即違反了(c)規則)。Object的這個函數返回的是裏面存儲的一個不可變的整數值,這個整數值隨着對象的創建從1開始退增。這個值是不可變的。但對於不同的輸入,其產生的分佈是不均勻的,因此效率並不高。
對於ValueType.GetHashCode,其返回第一個變量的HashCode值,比如public struct MyStruct {private string _msg; private int _d;}返回的是_msg的GetHashCode()值。因此值類型的GetHashCode函數非常依賴於第一個變量。如果第一個變量參於==函數的計算,即只有第一個變量相等時,兩個值類型纔會相等,則其滿足(a)規則,否則是不滿足的。如果第一個變量是不可變的,則其滿足(b)規則,否則是不滿足的。如果第一個變量的HashCode隨機均勻分佈,則其滿足(c)規則,否則也是不滿足的。
當我們重寫GetHashCode方法時,就要從以上三個規則出發。所有在==函數中參與的變量都要在GetHashCode函數參與HashCode的生成,雖然可以只要求==函數中的部分變量參與HashCode的生成,但勢必會造成分佈不均勻的情況。而參與HashCode生成的所有變量都要是不可變的,否則規則(b)就沒辦法滿足。而對於如何生成分佈均勻的哈希值,並沒有一個必然成立的規律。比較好的一個辦法是對所有參與生成HashCode的變量的哈希值作異或(XOR)的操作。

(13) foreach

儘量使用foreach來訪問集合裏面的每一個元素。因爲
(a) C#編譯器會根據集合元素的類型來生成不同的訪問代碼,如對於數組MyClass [] array來說,會生成類似for(int inex = 0; index < array.Length; index ++) { //Visit array[index] }的代碼,而對於實現在了IEnumerator接口的集合,則會生成類似IEnumerator it = foo.GetEnumerator(); while(it.MoveNext()) {...}的代碼。
(b) foreach是支持不同的數組上界和下界的,比如如果有些數組是從1開始的,則foreach也能夠很好地生成相應的代碼。
(c) foreach能夠支持多維數組。比如MyClass [,] multiArray = new MyClass[N, N],則foreach(MyClass var in multiArray)能夠訪問所有的二維數組的元素。
(d) 當修改了集合類型後,可以不修改代碼而能夠直接使用。

有個很有趣的現象,以下兩段代碼:
int [] foo = new int[100]
(a) for (int index = 0; index < foo.Length; index ++) {}
(b) int len = foo.Length;
      for (int index = 0; index < len; index ++) {}
(b)通過C#編譯器生成的代碼比(a)的代碼效率要低得多,其生成的代碼類似如下:
int len = foo.Lenght;
for (Int index = 0; index < len; index++)
{
    if (index < foo.Length) {...}
    else { throw IndexOutOfRangeException(); }
}
可以看到在循環裏面,生成的代碼又檢查了index是否小於foo.Length。因爲C#編譯器無法預知len是否在foo數組的合法範圍內,因此編譯器會生成額外的代碼去檢查index是否在合法的範圍內,否則拋出IndexOufOfRangeException異常。而對於(a)類代碼來說,因爲編譯器已經預知index會在0...foo.Length內,因此是合法的,不需要拋任何異常。

如果用戶自定義的集合類型也希望能夠使用foreach關鍵詞來訪問,則只需要通過以下三種方式之一就可以實現:(a) 提供public GetEnumerator()函數;(b) 實現IEnumerable接口;(c) 實現IEnumerator接口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章