構造函數的工作是為了初始化對象的所有成員,而一個類有多個構造函數又是一個非常常見的情景,所有這些構造函數難免會有類似乃至相同的邏輯,並且隨着時間的推移,成員變量的增加,功能的改變,構造函數的個數也會不斷上升。很多的開發人員一般會先編寫一個構造函數,然后將其代碼復制粘貼到其他的構造函數當中,以支持在類接口上定義的多個重寫構造函數.其實我們不應該這樣做,當發現多個構造函數包含類似的邏輯時,我們可以將其提取到一個公共的構造函數中。這樣既可以避免代碼重復也可以利用構造函數初始化器(constructor initializer)生成更高效的目標代碼。
閱讀目錄:
1.構造函數之間的相互調用
構造函數初始化器允許一個構造函數去調用另一個構造函數。通過構造函數之間的相互調用可以有效減少重復代碼,下面是一個構造函數之間相互調用的簡單示例:
1 public class MyClass 2 { 3 private List<string> coll; 4 private string name; 5 6 public MyClass():this(0,"") 7 { 8 } 9 10 public MyClass(int initialCount):this(initialCount,string.Empty) 11 { 12 } 13 14 public MyClass(int initialCount,string name) 15 { 16 coll=(initialCount>0)?new List<string>(initialCount):new List<string>(); 17 this.name=name; 18 } 19 }
2.使用默認參數減少重復代碼
我們還可以通過使用C# 4.0 的新特性——默認參數來進一步減少構造函數中的重復代碼。我們可以將上面的代碼所以的構造函數統一成一個,並為所有的可選參數指定默認值。如果將上面的代碼想使用重載來窮舉出同樣多的功能那么至少需要提供四個構造函數:一個無參數,一個接受initialCount參數,一個接受name參數(調用時需要使用具名參數調用),一個同時接受initialCount參數和name參數。可以看到:
隨着參數的增多,需要提供的重載也會直線上升,而使用默認參數可以有效減少構造函數的重復代碼,這是一種避免過多重載的良好機制
1 public class MyClass 2 { 3 private List<string> coll; 4 private string name; 5 private string p; 6 7 public MyClass() 8 : this(0, string.Empty) 9 { 10 } 11 12 //構造函數使用了可選參數,這里name參數使用""而不是更具語義的Empty因為:Empty不是編譯器常量,所以不能作為默認參數 13 public MyClass(int inititalCount = 0, string name = "") 14 { 15 coll = (inititalCount > 0) ? new List<string>(inititalCount) : new List<string>(); 16 this.name = name; 17 } 18 }
使用默認參數還是提供多個重載的構造函數是一個值得權衡的問題(參見:Effective C# 讀書筆記 條目10)。在上面的例子中,只需要后面使用可選參數的構造函數即可滿足我們的要求,這里還保留一個無參構造函數是因為:使用了new()約束的泛型類不支持所以參數都有默認值的構造函數,為了滿足new()約束,類必須提供顯示的無參構造函數。
3.共有構造函數 VS共有輔助方法
默認參數是C# 4.0的新特性,C#在4.0之前的版本中必須編寫每個需要支持的構造函數。這意味着很多的重復代碼,這時我們可以使用構造函數鏈,讓一個構造函數調用聲明在同一個類中的另一個構造函數,而不是像C++那也創建一個公有的輔助方法——因為創建公有的輔助方法會阻礙編譯器對代碼進行優化。我們看下面的代碼(不好):

1 public class MyClass 2 { 3 private List<string> coll; 4 private string name; 5 6 public MyClass() 7 { 8 CommonConstructor(0, ""); 9 } 10 11 public MyClass(int initialCount) 12 { 13 CommonConstructor(initialCount, ""); 14 } 15 16 public MyClass(int initialCount, string name) 17 { 18 CommonConstructor(initialCount, name); 19 } 20 21 /// <summary> 22 /// 一個所有構造函數公有的輔助方法 23 /// </summary> 24 /// <param name="count"></param> 25 /// <param name="name"></param> 26 private void CommonConstructor(int count, string name) 27 { 28 coll = (inititalCount > 0) ? new List<string>(inititalCount) : new List<string>(); 29 this.name = name; 30 } 31 }
上面的類使用了一個構造函數公有的輔助方法,和上一個使用默認參數的示例類似,只不過:一個是構造函數間的調用,一個是使用公有的輔助方法。不過在編譯時編譯器會為使用輔助方法版本的示例中添加一系列的代碼:即所有的成員初始化器(參見:Effective C# 讀書筆記 條目12),並且還會調用基類的構造函數,所以這回使我們的代碼效率大打折扣,並且當我們將name字段定義為readonly的時候會拋出編譯錯誤:
readonly 字段必須在聲明或構造函數中初始化。
最后,我們應該知道創建共有構造函和提供共有的輔助方法數的區別在於:
編譯器並不會生成多次調用基類構造函數的代碼,也不會講實例變量初始化器復制到每個構造函數中去。基類的構造函數會被最后一個構造函數調用一次:構造函數定義只能制定一個構造函數初始化器,要么使用this()委托給另一個構造函數,要么使用base()調用基類的構造函數,二者不可兼得。
4.CLR構造類型實例的過程
創建類型的第一個實例所執行的操作順序圖:
在第二個以及之后的實例將直接從第五步開始,因為類的構造器僅執行一次,而且第六步第七步將被優化,以便構造函數初始化器使編譯器移除重復的指令,執行順序如下圖:
5.小節
使用C#的構造函數初始化器可以很好的將這些公有的邏輯抽取出來,只需編寫一次,也只需要執行一次。到底是使用默認參數還是提供多個構造函數重載需要根據具體的使用場景來抉擇,一般情況下應該使用為一個公有的構造函數使用默認參數,並且給出的默認參數值必須永遠足夠合理,並且不能拋出異常。同時我們需要保證在實例的構造過程中對每個成員變量僅初始化一次,而實現這一點最好的方法就是,盡可能早的進行初始化工作。使用初始化器來初始化簡單資源,使用構造函數來初始化需要復雜邏輯的成員,同時將構造函數們的重復邏輯抽取到一個共有得構造函數中,以便減少重復代碼。