6.6.1 CLR如何调用虚方法、属性和事件
本节将讨论方法,但是本节中的讨论也与虚属性(virtual property)和虚事件(virtual event)相关。属性和事件实际上是作为方法实现的,这将在相应的章节进行示范。
方法表示在类型(静态方法)或者类型的实例(非静态方法)上执行操作的代码。所有的方法都有名称、签名和返回值(可能为void)。CLR允许类型定义多个名称相同的方法,只要这些方法具有不同的参数集或者不同的返回值。因此可以定义两个具有相同名称、相同参数的方法,只要这两个方法的返回值类型不同。但是,除了IL汇编语言外,好像没有其他语言具有该“特征”,大多数语言(包括C#语言在内)在确定惟一性时,要求方法的参数不同,而忽略方法返回值的类型。(C#在定义转换操作符方法时实际上放松了此限制,详见第8章的介绍。)
下面示范的Employee类定义了3个不同种类的方法:
internal class Employee {
//非虚实例方法
public Int32 GetYearsEmployed() { ... }
//虚方法(虚拟隐含着实例)
public virtual String GenProgressReport() { ... }
//静态方法
public static Employee Lookup(String name) { ... }
}
|
当编译器编译上述代码时,编译器在最后得到的程序集的方法定义表中写入三个条目,每个条目都有一个标记来表明方法是实例、虚的还是静态的。
所编写的代码调用这些方法时,编译器生成的调用代码检查方法定义的标记,以此来确定如何生成正确的IL代码,以便正确进行调用。CLR为方法的调用提供了以下两个IL指令:
IL指令call可以用来调用静态方法、实例方法和虚方法。使用call指令调用静态方法时,必须指定CLR要调用的方法的类型。使用call指令调用实例方法或者虚方法时,必须指定变量来引用对象。call指令假定变量不为null,换句话说,也就是变量本身的类型指出了用什么类型定义CLR要调用的方法。如果变量的类型没有定义方法,则检查基础类型来匹配方法。指令call通常用来非虚拟地调用虚方法。
IL指令callvirt用来调用实例方法和虚方法,而不能调用静态方法。使用callvirt指令调用实例方法或者虚方法时,必须指定变量来引用对象。使用IL指令callvirt指令调用非虚实例方法时,变量的类型指出了用什么类型定义CLR要调用的方法。使用IL指令callvirt调用虚实例方法时,CLR查找用来调用的对象的实际类型,然后多形式地调用方法。为了决定类型,用来调用的变量通常不能为null,换句话说,也就是编译该调用时,JIT编译器生成验证变量是否为null的代码,如果变量为null,callvirt指令引发CLR抛出一个NullReferenceException异常。这种额外的检查意味着IL指令callvirt的执行速度比call指令稍慢。注意,即使callvirt指令用来调用非虚实例方法时,也要执行这种变量是否为null的检查。
现在,我们将这两个调用指令放在一起,看看C#是如何使用这两个不同的IL指令的:
using System;
public sealed class Program{
public static void Main(){
Console.WriteLine();//调用一个静态方法
Object o = new Object();
o.GetHashCode();//调用一个虚实例方法
o.GetType();//调用一个非虚实例方法
}
}
|
编译上述代码,查看最后得到的IL代码,结果如下所示:
.method public hideby sigstatic void Main() cil managed{
.entrypoint
//代码大小 26(0x1a)
.maxstack 1
.locals init(objectV_0)
IL_0000: call void System.Console::WriteLine()
IL_0005: newobj instance void System.Object::.ctor()
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: callvirt instance int32 System.Object::GetHashCode()
IL_0011: pop
IL_0012: ldloc.0
IL_0013: callvirt instance class System.Type System.Object::GetType()
IL_0018: pop
IL_0019: ret
}//Program::Main方法结束
|
首先注意,C#编译器使用IL指令call调用Console的WriteLine方法,这与期望是相符的,因为WriteLine方法是静态方法。接着注意,C#编译器使用IL指令callvirt调用GetHashCode方法,这也与期望相符,因为GetHashCode方法是虚方法。最后注意,C#编译器同样使用IL指令callvirt调用GetType方法,这令人惊讶,因为GetType方法不是虚方法。但是,该调用可以正常调用,这是因为JIT编译上述代码时,CLR知道GetType方法不是虚方法,因此,JIT编译的代码自然将以非虚的方式调用GetType方法。
当然,问题在于,为什么C#编译器只生成call指令,而不是其他指令呢?答案就是因为C#的工作组决定JIT编译器应生成验证所使用的对象的代码,以确定调用不为null。这意味着对非虚的实例方法的调用要比其应有的运行速度稍慢一点,同样这也意味着下面的C#代码将抛出一个NullReferenceException异常。在其他一些编程语言中,下述代码将正确运行。
using System;
public sealed class Program{
public Int32 GetFive(){return5;}
public static void Main(){
Program p = null;
Int32 x = p.GetFive();//在C#中,会抛出一个NullReferenceException异常
}
}
|
从理论上讲,上述代码运行良好。的确,变量p为null,但是当调用非虚方法(如GetFive)时,CLR仅需要了解p的数据类型(p的数据类型为Program)。如果确实调用了GetFive方法,this参数的值将为null。因为GetFive方法中没有使用这个参数,因此不会抛出NullReferenceException异常。但是,因为C#编译器生成了一个callvirt指令,而不是生成了一个call指令,所以上述代码将抛出一个NullReferenceException异常并结束。
重要提示 如果将某个方法定义为非虚拟的,那么,将来永远不能将方法改为虚拟的。这是因为某些编译器会使用call指令而不是callvirt指令来调用非虚拟的方法。如果将方法从非虚拟的改为虚拟的,而且没有重新编译所涉及的代码,那么虚方法将被非虚拟地调用,致使应用程序产生无法预测的行为。如果所涉及的代码是用C#编写的,这就不是一个问题了,因为C#使用callvirt指令调用所有的实例方法。但是,如果所涉及的代码使用了不是C#的其他编程语言,这将产生问题。
有时,编译器会使用call指令代替callvirt指令来调用虚方法。起初这可能会令人惊讶,但是下述代码将说明为什么有时需要这么做:
internal class SomeClass {
//ToString是一个定义在基类Object中的虚方法
public override String ToString() {
//编译器使用IL指令'call'以非虚拟的方式调用object的ToString方法
//如果编译器用'callvirt'指令取代'call'指令,那么该方法将递归地调用其本身,直至堆栈溢出
return base.ToString();
}
}
|
调用虚方法base.ToString时,C#编译器生成一个call指令来确保非虚拟地调用基础类型中的ToString方法。需要这样做的原因在于:如果虚拟地调用ToString方法,那么调用将会递归执行,直至线程的堆栈溢出,这明显不是希望的结果。
编译器在调用值类型定义的方法时倾向于使用指令call,因为值类型是密封的。这意味着即使对于虚方法,也不存在多态,这将改善调用的性能,使调用速度更快。另外,值类型实例的本质保证了它永远不为null,因此永远不会抛出NullReferenceException异常。最后,如果虚拟地调用值类型的虚方法,那么,CLR为了在其内部引用方法表,需要引用值类型的类型对象,这需要对值类型进行装箱(boxing)。装箱给堆栈增加了更多的压力,强制进行更频繁的垃圾收集,使性能受到影响。
无论是否使用call指令和callvirt指令来调用实例方法或者虚方法,这些方法通常接收一个隐藏的this参数作为方法的第一个参数。this参数引用要进行操作的对象。
在设计类型的过程中,应尽量减小所定义的虚方法的数量。首先,调用虚方法的速度比调用非虚方法的速度要慢;其次,JIT编译器不能内联虚方法,这进一步影响了性能;第三,虚方法使组件的版本控制更脆弱,详见下节描述;第四,在定义基础类型时,通常需要提供一组有用的重载方法,如果希望这些方法是多态的,那么最好的办法就是将最复杂的方法虚拟化,而将所有有用的重载方法非虚拟化。附带提一下,遵循该原则同样会改善组件的版本控制能力,而不会影响派生类型的性能。下面给出示例:
public class Set {
private Int32 m_length = 0;
//这个有用的重载是非虚拟的
public Int32 Find(Object value) {
return Find(value, 0, m_length);
}
//这个有用的重载是非虚拟的
public Int32 Find(Object value, Int32 startIndex) {
return Find(value, 0, m_length);
}
//功能最丰富的方法是虚拟的,它可以被重写
public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) {
//重写的实际实现在此实现…
}
//其他方法在此实现
}
|
| 回书目 上一节 下一节 |
|
· Linux笔试面试题选摘测.. · 08年5月软考网管上午真.. · 性能测试从零开始 目录 · 08年5月软考网工上午真.. · 上周拒绝服务攻击(DDo.. · 08年5月各大网上书店及.. |
· 2008年5月24日软考试题.. · 软件设计师专家临考模.. · 上周网络管理员专家自.. · 网络工程师自测获奖名.. · 08年4月各大网上书店及.. · 系统分析师自测获奖名.. |
|
||||
| · ASP.NET开发教程 · 专题:ASP.NET 2.0基础.. · LAMP技术精解 · 服务器节能与绿色IT · ARP攻击防范与解决方案 · Linux 集群技术专题 · Windows集群服务应用 · CISSP认证成长之路 |
· SQL Server 2008/2005.. · SQL Server入门到精通 · 网络工程师职业规划与.. · 浏览器的战国时代 · 运营商封堵ADSL共享 中.. · 微软出价446亿美元收购.. · 技术人求职简历完备手册 · 开源虚拟化技术Xen |
|||
|
||||
| · SOA 面向服务架构 · SQL Server 2008/2005.. · Apache技术专题 · 三层交换技术专题 · SQL Server入门到精通 · Apache技术专题 · Windows集群服务应用 · 国际文档格式标准开战 |
· 路由器设置与口令恢复 · Linux 集群技术专题 · PHP开发应用手册 · SOA 面向服务架构 · 企业数据恢复指南 · 了解统一威胁管理(UTM).. · 专题:AIX操作系统管理.. · 访问控制列表(ACL)介绍 |
|||
|
||||
| · SQL Server入门到精通 · SQL Server 2008/2005.. · SOA 面向服务架构 · Apache技术专题 · 三层交换技术专题 · Apache技术专题 · 企业数据恢复指南 · Windows集群服务应用 |
· 路由器设置与口令恢复 · Linux 集群技术专题 · SOA 面向服务架构 · 了解统一威胁管理(UTM).. · 反垃圾邮件技术应用 · 访问控制列表(ACL)介绍 · ASP.NET开发教程 · PHP开发应用手册 |
|||