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

JerryXia 发表于 , 阅读 (607)

写构造函数是一个反复的工作。很多开发人员都是先写一个构造函数,然后复制粘贴到其它的构造函数里,以此来满足类的一些重载接口。希望你不是这样做的,如果是的,就此停止吧。有经验的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的。对静态成员和实例成员都是一样的。你的目标就是确保你希望执行的初始化代码只执行一次。使用预置方法来初始化简单的资源,使用构造函数来初始化一些具有复杂逻辑结构的成员。同样,为了减少重复尽可能的组织调用其它的构造函数。

添加新评论