首页 > 代码库 > 《CLR via C#》精髓:构造器

《CLR via C#》精髓:构造器

构造器包括实例构造器(Instance Constructor)和类型构造器(Type Constructor)。它们针对引用类型和值类型拥有不同的行为和准则。

实例构造器

引用类型

  1. 实例构造器用于初始化对象状态;
  2. 被调用前,对象所分配的内存总会被清零;
  3. 永远不会被继承;
  4. 如果没有显式地定义任何构造器,C#编译器会自动生成一个默认(无参)构造器,其实现代码将调用基类的无参构造器;
  5. 对于抽象类(abstract),编译器生成的默认构造器的可访问性为protected;
  6. 对于非抽象类,编译器生成的默认构造器的可访问性为public;
  7. 如果基类并没有提供无参构造器,派生类必须显式地调用基类的构造器,否则将出现编译错误;
  8. 对于静态类,编译器不会生成任何默认构造器;
  9. 如果派生类的构造器没有显式地调用基类的任何一个构造器,C#编译器将自动调用基类的默认构造器;
  10. 不要在构造器中调用任何虚方法,这将导致不可预知的行为;
  11. 如果类型定义字段时使用了内联初始化语法,编译器在调用基类构造器之前,会首先初始化所有使用了内联初始化语法的字段;
  12. 如果类型中有多个字段以及多个构造器,应避免对字段使用内联初始化语法,而是创建一个构造器完成字段的基本初始化,并让其它构造器显式地调用这个基本初始化构造器,完成特定字段的最终初始化。

值类型

  1. CLR总是允许创建值类型实例,并且无法阻止值类型被初始化。鉴于此,C#编译器不会为值类型生成默认无参构造器;
  2. 出于性能方面的考虑,对于包含在引用类型中的值类型字段,CLR不会调用它们的构造器;
  3. CLR确实允许为值类型定义构造器,但这些构造器只有被显式地调用时才会被执行;
  4. C#不允许为值类型定义无参构造器(但CLR允许),因此也就不允许值类型实例字段的内联初始化;
  5. 值类型中的任何构造器都必须完成值类型全部字段的初始化。

类型构造器

  1. 类型构造器可被应用于接口(C#不允许)、引用类型和值类型;
  2. 类型默认情况下没有类型构造器;
  3. 一个类型只能拥有一个类型构造器,且不能包含参数;
  4. 类型构造器总是private(C#会自动添加);
  5. 虽然可以为值类型定义类型构造器,但不要真的这么做,因为C#并不保证一定会调用它(原因见后文类型构造器何时被调用部分);
  6. 类型构造器初始化任何单例对象(Singleton)的好地方;
  7. 由于类型构造器由CLR调用(何时调用以及谁先谁后都由CRL决定),你应该永远避免编写依赖类型构造器按特定顺序调用的代码;
  8. 类型构造器中的代码只用来访问类型的静态字段,这也正是类型构造器的通常用途;
  9. 虽然C#不允许对值类型的实例字段进行内联初始化(原因见上文实例构造器——值类型部分的第4条),但允许对静态字段进行内联初始化;
  10. 如果对类型中静态字段进行了内联初始化,类型中也未定义类型构造器,C#编译器将自动为类型生成类型构造器,并在构造器内部为拥有内联初始化行为的静态字段生成初始化代码;
  11. 如果对类型中静态字段进行了内联初始化,并定义了类型构造器,C#编译器为类型构造器所生成的IL代码中将首先包括拥有内联初始化行为静态字段的初始化代码,然后才是类型构造器中显式定义的代码;
  12. 不要在类型构造器中调用基类的类型构造器,这种调用没有意义,因为类型的静态字段不会从基类继承或共享。

类型构造器何时被调用?

  • 对于引用类型,当类型的静态成员被访问,或者类型的实例被创建时,类型构造器将被调用;
  • 对于值类型,当类型的静态成员被访问,或者类型的一个实例构造器被调用时,类型构造器将被调用。

由此可以得出如下推论:

  1. 如果仅定义了引用类型变量但未实例化,那么类型构造器将不会被调用;
  2. 如果值类型实例被创建时没有调用实例构造器,那么类型构造器将不会被调用;
  3. 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#不保证一定会,但也不保证一定不会。