构造器包括实例构造器(Instance Constructor)和类型构造器(Type
Constructor)。它们针对引用类型和值类型拥有不同的行为和准则。
实例构造器
引用类型
- 实例构造器用于初始化对象状态;
- 被调用前,对象所分配的内存总会被清零;
- 永远不会被继承;
- 如果没有显式地定义任何构造器,C#编译器会自动生成一个默认(无参)构造器,其实现代码将调用基类的无参构造器;
- 对于抽象类(abstract),编译器生成的默认构造器的可访问性为protected;
- 对于非抽象类,编译器生成的默认构造器的可访问性为public;
- 如果基类并没有提供无参构造器,派生类必须显式地调用基类的构造器,否则将出现编译错误;
- 对于静态类,编译器不会生成任何默认构造器;
- 如果派生类的构造器没有显式地调用基类的任何一个构造器,C#编译器将自动调用基类的默认构造器;
- 不要在构造器中调用任何虚方法,这将导致不可预知的行为;
- 如果类型定义字段时使用了内联初始化语法,编译器在调用基类构造器之前,会首先初始化所有使用了内联初始化语法的字段;
- 如果类型中有多个字段以及多个构造器,应避免对字段使用内联初始化语法,而是创建一个构造器完成字段的基本初始化,并让其它构造器显式地调用这个基本初始化构造器,完成特定字段的最终初始化。
值类型
- CLR总是允许创建值类型实例,并且无法阻止值类型被初始化。鉴于此,C#编译器不会为值类型生成默认无参构造器;
- 出于性能方面的考虑,对于包含在引用类型中的值类型字段,CLR不会调用它们的构造器;
- CLR确实允许为值类型定义构造器,但这些构造器只有被显式地调用时才会被执行;
- C#不允许为值类型定义无参构造器(但CLR允许),因此也就不允许值类型实例字段的内联初始化;
- 值类型中的任何构造器都必须完成值类型全部字段的初始化。
类型构造器
- 类型构造器可被应用于接口(C#不允许)、引用类型和值类型;
- 类型默认情况下没有类型构造器;
- 一个类型只能拥有一个类型构造器,且不能包含参数;
- 类型构造器总是private(C#会自动添加);
- 虽然可以为值类型定义类型构造器,但不要真的这么做,因为C#并不保证一定会调用它(原因见后文类型构造器何时被调用部分);
- 类型构造器初始化任何单例对象(Singleton)的好地方;
- 由于类型构造器由CLR调用(何时调用以及谁先谁后都由CRL决定),你应该永远避免编写依赖类型构造器按特定顺序调用的代码;
- 类型构造器中的代码只用来访问类型的静态字段,这也正是类型构造器的通常用途;
- 虽然C#不允许对值类型的实例字段进行内联初始化(原因见上文实例构造器——值类型部分的第4条),但允许对静态字段进行内联初始化;
- 如果对类型中静态字段进行了内联初始化,类型中也未定义类型构造器,C#编译器将自动为类型生成类型构造器,并在构造器内部为拥有内联初始化行为的静态字段生成初始化代码;
- 如果对类型中静态字段进行了内联初始化,并定义了类型构造器,C#编译器为类型构造器所生成的IL代码中将首先包括拥有内联初始化行为静态字段的初始化代码,然后才是类型构造器中显式定义的代码;
- 不要在类型构造器中调用基类的类型构造器,这种调用没有意义,因为类型的静态字段不会从基类继承或共享。
类型构造器何时被调用?
- 对于引用类型,当类型的静态成员被访问,或者类型的实例被创建时,类型构造器将被调用;
- 对于值类型,当类型的静态成员被访问,或者类型的一个实例构造器被调用时,类型构造器将被调用。
由此可以得出如下推论:
- 如果仅定义了引用类型变量但未实例化,那么类型构造器将不会被调用;
- 如果值类型实例被创建时没有调用实例构造器,那么类型构造器将不会被调用;
- C#保证:引用类型和值类型的任何静态成员被访问前的某个时刻,类型构造器会被调用。
下例代码是针对上述结论和推论的验证:
using System;// 值类型
internal struct SomeValType {
// 类型构造器
static SomeValType() {
Console.WriteLine("This never gets displayed");
}// 实例构造器
public SomeValType(Int32 x) {
m_x = x;
}public Int32 m_x;
}// 引用类型
internal class SomeRefType {
// 类型构造器
static SomeRefType() {
Console.WriteLine("This will be display");
}public Int32 m_x;
}public sealed class Program {
public static void Main() {
SomeValType valType = new SomeValType();
valType.m_x = 123;SomeRefType refType = new SomeRefType();
refType.m_x = 456;
}
}
上述代码执行后输出结果为:
This will be display
很明显,引用类型(SomeRefType)的类型构造器被调用了,因为引用类型的实例被创建了;而值类型(SomeValType)的类型构造器没有被CLR调用,因为值类型的实例构造器没有被调用。正像类型构造器部分第5条所说的那样:“C#并不保证一定会调用它”,这也正符合推论2。
值类型的实例构造器没有被调用的原因就在于Main方法中值类型的实例化代码:
SomeValType valType = new SomeValType();
我们知道,值类型不允许定义默认无参实例构造器,上述代码只是定义一个SomeValType值类型实例并将其全部实例字段初始化为0或null,不会调用任何实例构造器。
如果我们确实希望值类型的类型构造器被调用,那么只需将上述代码修改为:
SomeValType valType = new SomeValType(789);
修改后的代码显式地调用了SomeValType值类型的一个实例构造器,输出结果如下:
This never gets displayed
This will be display
显然,值类型的类型构造器被调用了,因为现在已经符合“对于值类型,当类型的静态成员被访问,或者类型的一个实例构造器被调用时,类型构造器将被调用。”一句中“类型的一个实例构造器被调用”的要求。
上述试验其实又从反方向证明了类型构造器部分的第5条:“C#并不保证一定会调用它”——C#不保证一定会,但也不保证一定不会。