6.6.1 CLR如何调用虚方法、属性和事件
本节重点是方法,但讨论也与虚属性和虚事件密切相关。属性和事件实际作为方法实现,以后的章节会讨论他们。
方法
方法代表在类型或类型的实例上执行某些操作的代码。在类型上执行操作,称为静态方法;在类型的实例上执行操作,称为非静态方法。
所有方法都有名称、签名和返回类型。CLR允许类型定义多个同名方法,只要每个方法都有一组不同的参数或者一个不同的返回类型。
但除了IL汇编语言,没有任何利用了这一特点的语言。大多数语言包括C#,在判断方法的唯一性时,除了方法名之外,都只以参数为准,方法返回类型会被忽略。
C#在定义转换操作符方法时实际放宽了这个操作,详情在第八章。
以下Employee类定义了三种不同的方法
internal class Employee { //非虚实例方法 public Int32 GetYearsEmployed(){} //虚方法 public virtual String GetProgressReport{} //静态方法 public static Employee Lookup(String name){} }
编译以上代码,编译器会在程序集的方法定义表中写入3个记录项,每个记录项都用一组标值flag指明方法是实例方法、虚方法还是静态方法。
写代码调用这些方法,生成调用代码的编译器会检查方法定义的标值flag,判断应如何生成IL代码来正确调用方法。
CLR提供两个方法来调用指令
- call
该IL指令可调用静态方法、实例方法和虚方法。
用call指令调用静态方法,必须指定方法的定义类型。用call指令调用实例方法或虚方法,必须指定引用了对象的变量。
call指令假定该变量不为bull。换言之,变量本身的类型指明了方法的定义类型。如果变量的类型没有定义该方法,就检查基类型来查找匹配方法。
call指令经常用于以非虚方式调用虚方法。
- callvirt
该IL指令可调用实例方法和虚方法,不能调用静态方法。用callvirt指令调用实例方法或虚方法,必须指定引用了对象的变量。
用callvirt指令调用非虚实例方法,变量的类型指定了方法的定义类型。用callvirt指令调用虚实例方法,CLR调查发出调用的对象的实际类型,然后以多态方式调用方法。
为了确定类型,发出调用的变量决不能为null。换言之,编译这个调用时JIT编译器会生出代码来验证变量的值是不是null。
如果是,callvirt指令造成CLR抛出空引用异常。正是由于要进行这种额外的检查,所以callvirt指令的执行速度比call稍慢。
注意,即使callvirt指令调用的是非虚实例方法,也要执行这种null检查。
call和callvirt的实际使用
调用静态方法,IL会调用call指令,调用虚实例方法、非虚实例方法时,IL调用 callvirt方法。
这意味着,当对象为null时,调用对象方法会抛出空引用异常。
但编译器有时用call而不是callvirt调用虚方法,虽然刚开始有点难以理解,但下面代码证明了有时真的需要这样做
internal class SomeClass { //ToString是基类Object定义的虚方法 public override String ToString() { //编译器使用IL指令call //以非虚方式调用Object的ToString方法 //如果编译器用callvirt而不是 //那么该方法将递归调用自身,直至栈溢出 return base.ToString(); } }
调用虚方法base.ToString时,C#编译器生成call指令来确保以非虚方式调用基类的ToString方法。
这是必要的,因为如果以虚方式调用ToString,调用会递归执行。
值类型倾向使用call
编译器调用值类型定义的方法时倾向于使用call指令,因为值类型是密封的。
这意味着即使值类型含有虚方法也不要考虑多态性,这使调用更快。
此外,值类型实例的本质保证他永不为null,所以永不抛出空引用异常。
最后,如果以虚方式调用值类型中的虚方法,CLR要获取对值类型的类型对象的引用,以便引用(类型对象中的)方法表,这要求对值类型装箱。
装箱对堆造成更大压力,迫使更频繁的垃圾回收,使性能受到影响。
无论用call还是callvirt调用实例方法还是虚方法,这些方法通常接收隐藏的this实参作为方法的第一个参数。this实参引用要操作的对象。
类型的设计原则
类型设计的时候应尽量减少虚方法数量。首先调用虚方法的速度比调用非虚方法慢。其次,JIT编译器不能内嵌inline虚方法,这进一步影响性能。第三,虚方法使组建版本控制变得更脆弱。第四,定义基类型时,经常要提供一组重载的简便方法convenience method。如果希望这些方法是多态的,最好的办法就是使最复杂的方法成为虚方法,使所有重载的简便方法成为非虚方法。
遵循这个原则,还可在改善组件版本控制的同时,不至于对派生类型产生负面影响。