Effective C# 原则14:使用构造函数链

写构造函数是一个反复的工作。很多开发人员都是先写一个构造函数,然后复制粘贴到其它的构造函数里,以此来满足类的一些重载接口。希望你不是这样做的,如果是的,就此停止吧。有经验的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.

评论: 0 | 引用: 0 | 查看次数: 4888
发表评论
登录后再发表评论!