构造函数的工作是为了初始化对象的所有成员,而一个类有多个构造函数又是一个非常常见的情景,所有这些构造函数难免会有类似乃至相同的逻辑,并且随着时间的推移,成员变量的增加,功能的改变,构造函数的个数也会不断上升。很多的开发人员一般会先编写一个构造函数,然后将其代码复制粘贴到其他的构造函数当中,以支持在类接口上定义的多个重写构造函数.其实我们不应该这样做,当发现多个构造函数包含类似的逻辑时,我们可以将其提取到一个公共的构造函数中。这样既可以避免代码重复也可以利用构造函数初始化器(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++那也创建一个公有的辅助方法——因为创建公有的辅助方法会阻碍编译器对代码进行优化。我们看下面的代码(不好):
上面的类使用了一个构造函数公有的辅助方法,和上一个使用默认参数的示例类似,只不过:一个是构造函数间的调用,一个是使用公有的辅助方法。不过在编译时编译器会为使用辅助方法版本的示例中添加一系列的代码:即所有的成员初始化器(参见:Effective C# 读书笔记 条目12),并且还会调用基类的构造函数,所以这回使我们的代码效率大打折扣,并且当我们将name字段定义为readonly的时候会抛出编译错误:
readonly 字段必须在声明或构造函数中初始化。
最后,我们应该知道创建共有构造函和提供共有的辅助方法数的区别在于:
编译器并不会生成多次调用基类构造函数的代码,也不会讲实例变量初始化器复制到每个构造函数中去。基类的构造函数会被最后一个构造函数调用一次:构造函数定义只能制定一个构造函数初始化器,要么使用this()委托给另一个构造函数,要么使用base()调用基类的构造函数,二者不可兼得。
4.CLR构造类型实例的过程
创建类型的第一个实例所执行的操作顺序图:
在第二个以及之后的实例将直接从第五步开始,因为类的构造器仅执行一次,而且第六步第七步将被优化,以便构造函数初始化器使编译器移除重复的指令,执行顺序如下图:
5.小节
使用C#的构造函数初始化器可以很好的将这些公有的逻辑抽取出来,只需编写一次,也只需要执行一次。到底是使用默认参数还是提供多个构造函数重载需要根据具体的使用场景来抉择,一般情况下应该使用为一个公有的构造函数使用默认参数,并且给出的默认参数值必须永远足够合理,并且不能抛出异常。同时我们需要保证在实例的构造过程中对每个成员变量仅初始化一次,而实现这一点最好的方法就是,尽可能早的进行初始化工作。使用初始化器来初始化简单资源,使用构造函数来初始化需要复杂逻辑的成员,同时将构造函数们的重复逻辑抽取到一个共有得构造函数中,以便减少重复代码。