在.Net的类型当中,包含三种成员:字段、方法和嵌套类型。字段是一个命名的存储单元,隶属于所声明的类型。方法是一个命名的操作,可以被调用和执行。嵌套类型则是一个辅助类型,被定义为声明类型的实现的一部分。今天我们讨论的构造方法则是一种特殊的方法。
构造方法是一种将类型实例化的一种特殊方法,系统在创造类型实例的时候,首先会根据类型分配内存,然后初始化类型对象指针和同步索引块,在执行完这两步之后,便开始调用构造函数。
构造方法不能被继承,不能用virtual、new、override、sealed和abstract修饰,也没有返回值。
构造方法分为两种:类型构造方法(.cctor)与实例构造方法(.ctor)。
实例构造方法
Ps:在本段当中描述的字段为非静态字段
实例构造方法可以根据需求来创建有效的实例,每个类型可以由多个构造方法,但是每个构造器必须要有一个唯一的签名。并不是所有的实例创建都会调用实例构造方法,例如通过按位复制的时候(Object.MemberwiseClone方法),由前文可知,系统在创建实例的时候,首先会分配内存,然后初始化类型对象指针和同步索引块,按位复制时,系统不会执行构造函数,而是直接按位将信息写到当前类型的内存当中完成新实例的构造。
我们知道,我们可以在类型声明字段的时候,直接定义当前字段的值,然后由上面可知,在系统在创建类型的三步当中,并没有明确指出对于直接声明并赋值的字段是如何执行的,我们可下面的代码:
class Sub
{
private int subTest = 200;
public Sub() { }
}
将其执行,并使用IL反编译之后可以看到如下:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// 代码大小 21 (0x15)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4 0xc8
IL_0006: stfld int32 TestDotNetConstructor.Sub::subTest
IL_000b: ldarg.0
IL_000c: call instance void [mscorlib]System.Object::.ctor()
IL_0011: nop
IL_0012: nop
IL_0013: nop
IL_0014: ret
} // end of method Sub::.ctor
由上面的代码我们可以看出,对于这类字段的赋值,CLR将其放在了构造方法当中。事实上,CLR按照字段的声明顺序在编译时,添加到构造方法当中,在显式的方法体之前调用字段的初始化表达式。在正常情况下,会将初始化表达式添加到所有的构造方法当中(注意:是在正常情况下)。
实例构造方法还支持链式构造,即如下所示的方法执行构造
public Sub(int i) : this() {subTest = 100; }
其执行顺序如下所示:
.method public hidebysig specialname rtspecialname
instance void .ctor(int32 i) cil managed
{
// 代码大小 18 (0x12)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void TestDotNetConstructor.Sub::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldarg.0
IL_0009: ldc.i4.s 100
IL_000b: stfld int32 TestDotNetConstructor.Sub::subTest
IL_0010: nop
IL_0011: ret
} // end of method Sub::.ctor
即系统会首先执行Sub(),然后再执行Sub(int i)当中的方法
当当前类型从另外一个类型继承的时候,构造方法又是怎样执行的呢?我们引入如下的Base类:
class Base
{
private int baseTest = 100;
public Base(){}
public Base(int i){baseTest = i;}
}
并让Sub类继承此类,然后看如下的构造方法:
public Sub(int i, string s){}
public Sub() { }
使用反编译之后,我们可以看到这两个方法在的前两句都是
IL_0000: ldarg.0
IL_0001: call instance void TestDotNetConstructor.Base::.ctor()
即在正常情况下系统会在所有的构造方法显式的方法体之前执行父类默认的构造函数。
在我们前面的描述当中,我们使用了两个“在正常情况下”,一个是在正常情况下,会将初始化表达式添加到所有的构造方法当中(声明并赋值字段时),另一个就是在上面我们描述执行父类构造函数时,那么什么是非正常情况呢?
看下面的语句:
public Sub(int i) : this() {subTest = 100; }
其IL为
.method public hidebysig specialname rtspecialname
instance void .ctor(int32 i) cil managed
{
// 代码大小 18 (0x12)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void TestDotNetConstructor.Sub::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldarg.0
IL_0009: ldc.i4.s 100
IL_000b: stfld int32 TestDotNetConstructor.Sub::subTest
IL_0010: nop
IL_0011: ret
} // end of method Sub::.ctor
我想大家看到这里都明白了,CLR是很聪明的,如果当前的构造方法执行了链式构造,那么CLR在编译的时候,就不会将声明并赋值的表达式和对父类的构造添加到当前的构造方法当中。这样就避免了重复执行,提高了效率。
由于CLR允许创建值类型的实例并且直接初始化(无法阻止),因此值类型的构造方法与引用类型的构造方法相比有些不同,首先值类型不会生成默认的无参数的构造器,也不允许开发者自己声明无参数的构造方法,如果开发者强行定义,则会在编译的时候报错:错误1结构不能包含显式的无参数构造函数。事实上,CLR自己也不会自动生成无参数的构造器,也就是说值类型不存在无参数构造器。
虽然无法定义无参数构造函数,但是开发者可以定义带参数的构造函数,但是在定义带参数的构造函数的时候,必须对当前值类型内部所有的字段进行初始化(个人猜想:估计CLR在对其进行分配内存的时候,根据构造方法进行内存的分配了)。
类型构造器(.cctor)
除了实例构造器之外,CLR还支持类型构造器,也成为静态构造器,类型构造器永远没有返回值和参数,其前方的修饰词也只能有一个(static),在前面我们知道对于直接声明并赋值的非静态字段,其赋值表达式添加到了实例构造方法当中,对于静态声明并且赋值的字段,其赋值表达式添加到了类型构造方法当中,原理相同,在此不再进行赘述。
如下方法:
private static int subStaticTest = 100;
static Sub() { }
则IL如下所示:
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// 代码大小 10 (0xa)
.maxstack 8
IL_0000: ldc.i4.s 100
IL_0002: stsfld int32 TestDotNetConstructor.Sub::subStaticTest
IL_0007: nop
IL_0008: nop
IL_0009: ret
} // end of method Sub::.cctor
注意:我们可以看到CLR自动将类型构造方法编译为了private的。
另外在值类型当中不允许直接声明并赋值字段,但是对于静态字段是可以的。
在默认的情况之下,类型构造器会在首次访问类型的任何成员之前执行,但是开发者可以使用TypeAttributes.BeforeFieldInit属性来推迟类型初始化方法的执行,使用了此属性(Attribute)之后,类型初始化器会在第一次访问静态字段的时候执行。这就意味着在使用了此属性的类型上调用静态方法的时候,并不一定会执行类型构造器。