建構函式的工作是為了初始化對象的所有成員,而一個類有多個建構函式又是一個非常常見的情景,所有這些建構函式難免會有類似乃至相同的邏輯,並且隨著時間的推移,成員變數的增加,功能的改變,建構函式的個數也會不斷上升。很多的開發人員一般會先編寫一個建構函式,然後將其代碼複製粘貼到其他的建構函式當中,以支援在類介面上定義的多個重寫建構函式.其實我們不應該這樣做,當發現多個建構函式包含類似的邏輯時,我們可以將其提取到一個公用的建構函式中。這樣既可以避免代碼重複也可以利用建構函式初始化器(constructor initializer)產生更高效的目標代碼。
閱讀目錄:
1.建構函式直接的相互調用
2.使用預設參數減少代碼
3.共有建構函式VS共有輔助方法
4.CLR構造類型執行個體操作過程
5.小節
6.進一步閱讀&參考
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++那也建立一個公有的輔助方法——因為建立公有的輔助方法會阻礙編譯器對代碼進行最佳化。我們看下面的代碼(不好):
View Code
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#的建構函式初始化器可以很好的將這些公有的邏輯抽取出來,只需編寫一次,也只需要執行一次。到底是使用預設參數還是提供多個建構函式重載需要根據具體的使用情境來抉擇,一般情況下應該使用為一個公有的建構函式使用預設參數,並且給出的預設參數值必須永遠足夠合理,並且不能拋出異常。同時我們需要保證在執行個體的構造過程中對每個成員變數僅初始化一次,而實現這一點最好的方法就是,儘可能早的進行初始化工作。使用初始化器來初始化簡單資源,使用建構函式來初始化需要複雜邏輯的成員,同時將建構函式們的重複邏輯抽取到一個共有得建構函式中,以便減少重複代碼。
參考資料&進一步閱讀
命名實參和可選實參
new約束
型別參數的約束
readonly(C# 參考)