Effective C# 原則14:使用建構函式鏈(譯)

來源:互聯網
上載者:User

Effective C# 原則14:使用建構函式鏈
Item 14: Utilize Constructor Chaining

寫建構函式是一個反覆的工作。很多開發人員都是先寫一個建構函式,然後複製粘貼到其它的建構函式裡,以此來滿足類的一些重載介面。希望你不是這樣做的,如果是的,就此停止吧。有經驗的C++程式可能會用一個輔助的私人方法,把常用的演算法放在裡面來構造對象。也請停止吧。當你發現多重建構函式包含相同的邏輯時,取而代之的是把這些邏輯放在一個常用的建構函式裡。你可以得避免代碼的重複的好處,並且建構函式初始化比對象的其它代碼執行起來更高效。C#編譯器把建構函式的初始化識別為特殊的文法,並且移除預置方法中重複的變數和重複的基類建構函式。結果就是這樣的,你的對象最終執行最少的代碼來合理的初始化對象。你同樣可以寫最少的代碼來把負責委託給一個常用的建構函式。建構函式的預置方法充許一個建構函式調用另一個建構函式。這是一個簡單的例子:

public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Name of the instance:
  private string  _name;

  public MyClass() :
    this( 0, "" )
  {
  }

  public MyClass( int initialCount ) :
    this( initialCount, "" )
  {
  }

  public MyClass( int initialCount, string name )
  {
    _coll = ( initialCount > 0 ) ?
      new ArrayList( initialCount ) :
      new ArrayList();
    _name = name;
  }
}

C#不支援帶預設值的參數,C++是很好的解決這個問題的(譯註:C++可以讓參數有預設的值,從而有效減少函數的重載)。你必須重寫每一個特殊的建構函式。對於這樣的建構函式,就意味著大量的代碼重複工作。可以使用建構函式鏈來取代常規的方法。下面就是一些常規的低效率的建構函式邏輯:
public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Name of the instance:
  private string  _name;

  public MyClass()
  {
    commonConstructor( 0, "" );
  }

  public MyClass( int initialCount )
  {
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    _name = name;
  }
}

這個版本看上去是一樣的,但產生的效率遠不及對象的其它代碼。為了你的利益,編譯器為建構函式添加了一些代碼。添加了一些代碼來初始化所有的變數(參見原則12)。它還調用了基類的建構函式。當你自己寫一些有效函數時,編譯器就不會添加這些重複的代碼了。第二個版本的IL代碼和下面寫的是一樣的:
// Not legal, illustrates IL generated:
public MyClass()
{
  private ArrayList _coll;
  private string  _name;

  public MyClass( )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( 0, "" );
  }

  public MyClass (int initialCount)
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    _name = name;
  }
}

如果你用第一個版本寫建構函式,在編譯看來,你是這樣寫的:
// Not legal, illustrates IL generated:
public MyClass()
{
  private ArrayList _coll;
  private string  _name;

  public MyClass( )
  {
    // No variable initializers here.
    // Call the third constructor, shown below.
    this( 0, "" ); // Not legal, illustrative only.
  }

  public MyClass (int initialCount)
  {
    // No variable initializers here.
    // Call the third constructor, shown below.
    this( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    _counter = initialCount;
    _name = Name;
  }
}

不同之處就是編譯器沒有產生對基類的多重調用,也沒有複製執行個體變數到每一個建構函式內。實際上基類的建構函式只是在最後一個建構函式裡被調用了,這同樣很重要:你不能包含更多的建構函式預置方法。在這個類裡,你可以用this()把它委託給另一個方法,或者你可以用base()調用基類的構造。但你不能同時調用兩個。

還不清楚建構函式預置方法嗎?那麼考慮一下唯讀常量,在這個例子裡,對象的名字在整個生命期內都不應該改變。這就是說,你應該把它設定為唯讀。如果使用輔助函數來構造對象就會得到一個編譯錯誤:

public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Number for this instance
  private int       _counter;
  // Name of the instance:
  private readonly string  _name;

  public MyClass()
  {
    commonConstructor( 0, "" );
  }

  public MyClass( int initialCount )
  {
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    // ERROR changing the name outside of a constructor.
    _name = name;
  }
}

C++程式會把這個_name留在每一個建構函式裡,或者通常是在輔助函數裡把它丟掉。C#的建構函式預置方法提供了一個好的選擇,幾乎所有的瑣碎的類都包含不只一個建構函式,它們的工作就是初始化對象的所有成員變數 。這是很常見的,這些函數在理想情況下有相似的共用邏輯結構。使用C#構造預置方法來產生這些常規的演算法,這樣就只用寫一次也只執行一次。

這是C#裡的最後一個關於物件建構的原則,是時候覆習一下,一個類型在構造時的整個事件順序了。你須要同時明白一個對象的操作順序和預設的預置方法的順序。你構造過程中,你應該努力使所有的成員變數只精確的初始化一次。最好的完成這個目標的方法就是儘快的完成變數的初始化。這是某個類型第一次構造一個執行個體時的順序:
1、靜態變數儲存位置0。
2、靜態變數預置方法執行。
3、基類的靜態建構函式執行。
4、靜態建構函式執行。
5、執行個體變數儲存位置0。
6、執行個體變數預置方法執行。
7、恰當的基類執行個體建構函式執行。
8、執行個體建構函式執行。
後續的同樣類型的執行個體從第5步開始,因為類的預置方法只執行一次。同樣,第6和第7步是最佳化了的,它可以讓編譯器在建構函式預置方法上移除重複的指令。

C#的編譯器保證所有的事物在初始化使用同樣的方法來產生。至少,你應該保證在你的類型建立時,對象佔用的所有記憶體是已經置0的。對靜態成員和執行個體成員都是一樣的。你的目標就是確保你希望執行的初始化代碼只執行一次。使用預置方法來初始化簡單的資源,使用建構函式來初始化一些具有複雜邏輯結構的成員。同樣,為了減少重複儘可能的組織調用其它的建構函式。
=============================   

Item 14: Utilize Constructor Chaining
Writing constructors is often a repetitive task. Many developers write the first constructor and then copy and paste the code into other constructors, to satisfy the multiple overrides defined in the class interface. Hopefully, you're not one of those. If you are, stop it. Veteran C++ programmers would factor the common algorithms into a private helper method. Stop that, too. When you find that multiple constructors contain the same logic, factor that logic into a common constructor instead. You'll get the benefits of avoiding code duplication, and constructor initializers generate much more efficient object code. The C# compiler recognizes the constructor initializer as special syntax and removes the duplicated variable initializers and the duplicated base class constructor calls. The result is that your final object executes the minimum amount of code to properly initialize the object. You also write the least code by delegating responsibilities to a common constructor.

Constructor initializers allow one constructor to call another constructor. This example shows a simple usage:

public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Name of the instance:
  private string  _name;

  public MyClass() :
    this( 0, "" )
  {
  }

  public MyClass( int initialCount ) :
    this( initialCount, "" )
  {
  }

  public MyClass( int initialCount, string name )
  {
    _coll = ( initialCount > 0 ) ?
      new ArrayList( initialCount ) :
      new ArrayList();
    _name = name;
  }
}

C# does not support default parameters, which would be the preferred C++ solution to this problem. You must write each constructor that you support as a separate function. With constructors, that can mean a lot of duplicated code. Use constructor chaining instead of creating a common utility routine. Several inefficiencies are present in this alternative method of factoring out common constructor logic:

public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Name of the instance:
  private string  _name;

  public MyClass()
  {
    commonConstructor( 0, "" );
  }

  public MyClass( int initialCount )
  {
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    _name = name;
  }
}

That version looks the same, but it generates far less efficient object code. The compiler adds code to perform several functions on your behalf in constructors. It adds statements for all variable initializers (see Item 12). It calls the base class constructor. When you write your own common utility function, the compiler cannot factor out this duplicated code. The IL for the second version is the same as if you'd written this:

// Not legal, illustrates IL generated:
public MyClass()
{
  private ArrayList _coll;
  private string  _name;

  public MyClass( )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( 0, "" );
  }

  public MyClass (int initialCount)
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    _name = name;
  }
}

If you could write the construction code for the first version the way the compiler sees it, you'd write this:

// Not legal, illustrates IL generated:
public MyClass()
{
  private ArrayList _coll;
  private string  _name;

  public MyClass( )
  {
    // No variable initializers here.
    // Call the third constructor, shown below.
    this( 0, "" ); // Not legal, illustrative only.
  }

  public MyClass (int initialCount)
  {
    // No variable initializers here.
    // Call the third constructor, shown below.
    this( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    // Instance Initializers would go here.
    object(); // Not legal, illustrative only.
    _counter = initialCount;
    _name = Name;
  }
}

The difference is that the compiler does not generate multiple calls to the base class constructor, nor does it copy the instance variable initializers into each constructor body. The fact that the base class constructor is called only from the last constructor is also significant: You cannot include more than one constructor initializer in a constructor definition. You can delegate to another constructor in this class using this(), or you can call a base class constructor using base(). You cannot do both.

Still don't buy the case for constructor initializers? Then think about read-only constants. In this example, the name of the object should not change during its lifetime. This means that you should make it read-only. That causes the common utility function to generate compiler errors:

public class MyClass
{
  // collection of data
  private ArrayList _coll;
  // Number for this instance
  private int       _counter;
  // Name of the instance:
  private readonly string  _name;

  public MyClass()
  {
    commonConstructor( 0, "" );
  }

  public MyClass( int initialCount )
  {
    commonConstructor( initialCount, "" );
  }

  public MyClass( int initialCount, string Name )
  {
    commonConstructor( initialCount, Name );
  }

  private void commonConstructor( int count,
    string name )
  {
    _coll = (count > 0 ) ?
      new ArrayList(count) :
      new ArrayList();
    // ERROR changing the name outside of a constructor.
    _name = name;
  }
}

C++ programmers just live with this and initialize _name in all constructors, or they cast away constness in the utility routine. C#'s constructor initializers provide a better alternative. All but the most trivial classes contain more than one constructor. Their job is to initialize all the members of an object. By their very nature, these functions have similar or, ideally, shared logic. Use the C# constructor initializer to factor out those common algorithms so that you write them once and they execute once.

This is the last item about object initialization in C#. That makes it a good time to review the entire sequence of events for constructing an instance of a type. You should understand both the order of operations and the default initialization of an object. You should strive to initialize every member variable exactly once during construction. The best way for you to accomplish this is to initialize values as early as possible. Here is the order of operations for constructing the first instance of a type:

1.  Static variable storage is set to 0.

 
2.  Static variable initializers execute.

 
3.  Static constructors for the base class execute.

 
4.  The static constructor executes.

 
5.  Instance variable storage is set to 0.

 
6.  Instance variable initializers execute.

 
7.  The appropriate base class instance constructor executes.

 
8.  The instance constructor executes.

 Subsequent instances of the same type start at step 5 because the class initializers execute only once. Also, steps 6 and 7 are optimized so that constructor initializers cause the compiler to remove duplicate instructions.

The C# language compiler guarantees that everything gets initialized in some way when an object gets created. At a minimum, you are guaranteed that all memory your object uses has been set to 0 when an instance is created. This is true for both static members and instance members. Your goal is to make sure that you initialize all the values the way you want and execute that initialization code only once. Use initializers to initialize simple resources. Use constructors to initialize members that require more sophisticated logic. Also factor calls to other constructors, to minimize duplication.
 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.