最近学习了一下IL语言,其实知道C#中是有默认的构造函数的,如果不显式实现,编译器会自动给加上,例如下面这个简单的函数:

using System;
namespace Castor
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Greeting!");
Console.Read();
}
}
}

编译之后得到的源文件用Reflector查看:

从.ctor看对象的创建 - Castor - 趁年轻,多折腾

查看其代码,如果是C#,其实什么也看不到,就是一个空的函数体,真正能够看出端倪还得看IL语言:

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ret
}

ldarg.0的作用是将方法的第0个参数压入堆栈,而实例的this指针总是第0个参数,所以ldarg.0将this压入栈,然调用了基类Object类的构造函数,最后返回。这个地方比较有意思的是IL下的构造函数返回值类型是void的,而在C#中构造函数是不能有返回值的。

如果我们出于好心,给它提供一个自己的构造函数,例如,空构造函数public Program(){},IL语句会发生什么样的变化呢?直觉上看,应该是没有任何变化才对,可是,结果还是有一点点变化:

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: nop
L_0007: nop
L_0008: nop

L_0009: ret
}

其实nop指令就是表示不执行任何操作的,删除不会有任何问题,可是为什么会多了三个nop指令呢?窃以为是微软为了内部区分该构造函数是编译器自动生成的还是由用户提供的,当然到底是怎么回事,我还真不知道。

如果我们修改一下构造函数,使其和默认构造函数有不一样的签名,会有什么现象发生呢?例如我们提供一个成员变量int i,然后加入如下构造函数:

public Program(int _i)
{
i = _i;
}

可以发现默认的构造函数不见了:

从.ctor看对象的创建 - Castor - 趁年轻,多折腾
这也和C#中的性质保持一致:如果显式提供了构造函数,则编译器不再自动添加缺省构造函数。
看看带参数的构造函数中IL语言的变化:

.method public hidebysig specialname rtspecialname instance void .ctor(int32 _i) cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: nop
L_0007: nop
L_0008: ldarg.0
L_0009: ldarg.1
L_000a: stfld int32 Castor.Program::i

L_000f: nop
L_0010: ret
}

注意从L_0008开始的语句,ldarg.0是this,ldarg.1是第一个参数,其实就是这里的形参_i,然后设置字段i。另外,这个构造函数还说明,类在实例化的时候是先实例化了基类的构造函数,然后再进行修改成员变量等其他操作。

等一下,我们知道在C#中定义一个成员变量的时候是可以对其初始化的,那么这个初始化的工作在什么时候完成的呢?还是用最开始的代码:

using System;
namespace Castor
{
class Program:Object
{
int i=100;//请注意,i已经初始化
static void Main(string[] args)
{
Console.WriteLine("Greeting!");
Console.Read();
}
}
}

这个时候IL语言的变化是怎么样的呢?

.method public hidebysig specialname rtspecialname instance void .ctor(int32 _i) cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldc.i4.s 100
L_0003: stfld int32 Castor.Program::i

L_0008: ldarg.0
L_0009: call instance void [mscorlib]System.Object::.ctor()
L_000e: nop
L_000f: nop
L_0010: ldarg.0
L_0011: ldarg.1
L_0012: stfld int32 Castor.Program::i
L_0017: nop
L_0018: ret
}

可见初始化成员变量的流程是这样的:在构造函数中先加载this,然后马上将已经初始化的成员变量赋值,然后是调用基类的构造函数。最后才是设置实例的成员变量值。

最后还有一个问题,对于修饰符为const的字段,构造函数是怎么处理的呢?

看看下面的代码:

using System;
namespace Castor
{
class Program : Object
{
int i = 100;
const int c = 40;
static void Main(string[] args)
{
Console.WriteLine("Greeting!");
Console.Read();
}
}
}

依据前面的检验,这会由编译器提供一个默认的构造函数,来看看其中的代码:

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldc.i4.s 100
L_0003: stfld int32 Castor.Program::i
L_0008: ldarg.0
L_0009: call instance void [mscorlib]System.Object::.ctor()
L_000e: nop
L_000f: ret
}

你找不到任何代码对成员c进行的操作,那么程序在什么时候设置了c的值呢?其实是在编译期间就完成了,而不需要运行代码。

来看看成员c的反汇编代码:

.field private static literal int32 c = int32(40)

c具有的属性很多,首先是静态的,然后还是字面值,这说明c在编译期间已经被编译器替换为了常量了,这也是为什么申明为const的字段一定要提供一个值的要求。

从而可以得出结论:构造函数不处理const类型的字段,该字段在编译过程中已被替换为对应的字面值。

看来,一个简单的构造函数,也是有很多名堂的。后面我打算分析一下.cctor,即类型构造函数。