MSIL 初级读本 第四部分:定义类型成员
上一篇文章中介绍了定义类型的基本语法。使用 .class 指示符,你可以定义值类型或者引用类型。通过选择各种修饰符可以获得对类型的完全控制。现在让我们来探讨一下如何声明类型的成员。为了简单起见,有时我会省略类型的定义。
构造函数
让我们从初始化函数开始——在C++或C#这样的语言中,既是构造函数。CLI 支持两种初始化函数:类型初始化函数和实例初始化函数。类型初始化函数是一个相当便利的设施,它可以轻易解决使用原生 C++ 实现单件模式时经常遇到的有关多线程的麻烦。指定类型的类型初始化函数仅运行一次,并且运行时保证在该类的任何成员被访问之前,完成该函数的执行。
.cctor 和 .ctor 是保留的方法名称. 类型初始化函数(并非必须实现)被定义为一个名为 .cctor 的没有返回值的静态函数。在 C++/CLI 和 C# 中,类型初始化函数既是静态构造函数。
实例初始化函数经常被简称为构造函数,它负责初始化类型的某一个实例。构造函数被定义为一个名为 .ctor 的没有返回值的函数。当使用 newobj 指令创建一个新实例时,其构造函数会被调用。例如:
如果使用了上一段代码的类型定义,则执行结果是在控制台上得到了如下输出:
newobj 指令分配指定类型的一个新实例,并且将其所有字段初始化为默认值,然后调用匹配指定签名的构造函数,确保第一个参数指向新创建的实例。一旦构造函数执行完毕,新实例的引用将被压栈,以供调用者访问。
方法
CLI 使用统一的方式定义普通方法、构造函数和属性(使用一些附加的原数据定义属性),很多关于方法的描述也同样适用于实例构造函数和属性的访问器。你可以通过方法修饰符来进行一些控制。使用 static 修饰符来定义静态方法。它仅与类型而非实例相关;用 instance 修饰符替换 static 即可声名实例方法。IL 将 instance 作为默认修饰符,所以基本上用不着进行显式指定。
然而调用方法时的情况刚好相反。除非显式指出,否则 call 指令会默认被调用的方法是静态方法,例如:
请记得在调用实例方法之前先将实例的对象引用压栈。
虚函数的调用是面向对象设计的重要组成部分,CLI 为此提供了极大的灵活性,你可以控制使用对象的动态或静态类型来调用方法,以及在派生类中如何重载这些行为。按照 C++ 的概念来说,一个对象的静态类型指的是编译时期确定的类型,而动态类型指的是运行时确定的类型。在 MSIL 中使用虚函数时,应当记住如何声明一个实例方法以支持虚函数调用 以及如何调用该方法。当然,静态方法不可能是虚函数。通过 virtual 修饰符来指定虚方法。例如:
House 类有一个名为 Buy 的虚方法。TownHouse 类继承了 House 类,并且有一个具有相同名称的虚方法,即 TownHouse::Buy 重载了 House::Buy。如何告知运行时选择哪个方法呢?显然,实现多态需要在运行时根据具体实例类型来决定调用的方法。到目前为止,我们都是使用 call 指令来调用方法的,它总是调用指定的方法而不管一个对象的动态类型如何。而 callvirt 指令则允许在运行时根据实际类型来调用特定的的虚函数实现。例如:
执行后将得到如下输出:
前两个调用仅关心静态的、编译时期的类型。同样第三个调用也是如此,即使此时栈上的对象引用是 TownHouse 类型的。而第四个调用中使用了 callvirt 指令。方法的调用显示了多态性。
如果在派生类中声明了一个虚方法同时又不想重载继承自基类的具有相同签名的方法,你可以在派生类的新虚方法上使用 newslot 修饰符。这将在该类型的虚函数表上添加一个指针。