CLR虚方法调用原理

我们知道CLR会为每个类型创建一个”方法表“。方法表的详细布局方式微软没有公开发布,因此只能依据自己的理解给出一个大致的方法表, 并不一定符合CLR实情。

    类型方法表的前面是一些CLR用于管理类型的控制信息,紧接着是一个表明此类型的方法总数的表项(即图中的方法槽个数),类型用的方法被分为几部分依次排列,一般来说,虚方法都集中在表的前部,因为这些方法可能会以多态方式被调用。表的中部是类型定义的”普通实例方法“,后部是类型定义的静态成员(包括静态方法与静态字段)。

     方法表的最后一部分是”接口偏移表“,这里面存放了本类型所实现的所有接口的清单列表。由于每个接口通常都会定义几个方法,因此接口偏移表中的表项有两部分组成:

   第一部分是接口的方法表指针,它引用接口的方法表,所以利用这一指针CLR可以获取接口所定义的方法清单。
   第二部分是一个索引。对于一个实现了特定接口的类型,它一定定义了对应的实例方法。这些方法被”捆“在一起,集中的放到类型方法表的特定区域中,此区域第一个方法在类型方法表中的索引被”记录“了下来,保存在接口偏移表与此接口相对应的表项中。

构成继承关系的方法表

class Parent
{
    public void p() { }
}

class Child : Parent
{
    public void c() { }
}

对应的内存布局如图

调用示例代码

Child c = new Child();
c.c();  //调用自身定义的方法
c.p();  //调用基类定义的方法

上述代码对应的IL指令如下:

可以看到编译时三条c#语句分别被绑定到了相应方法表的特定方法上。当CLR执行这三条IL指令时,将会根据要调用方法的标识到相应的方法表中去查找对应的方法。

  当子类隐藏了父类的同名方法时,(比如上例中Child类用new关键字隐藏了父类的p方法),内存布局大同小异,只不过现在Child的方法表中多了一个p方法罢了。

测试代码如下:

Child c = new Child();
c.p();  //调用隐藏了基类定义的方法
(c as Parent).p();  //调用基类定义的方法

上述代码对应的IL指令如下:

可以看到c#编译器能够正确地判断出应该调用哪个方法,并生成了相应的callvirt指令。

使用继承的虚方法调用原理

  调用虚方法时具体调用哪个方法不是在编译时确定的,而是在运行时根据对象的真实类型而定的。因此CLR对于虚方法调用采用了动态分派的方法。

如下测试代码:

class Parent
{
    public virtual void virtualMtd() { }
}

class Child : Parent
{
    public override void virtualMtd() { }
}

Parent p = new Parent();
p.virtualMtd();    //通过基类变量调用虚方法
Child c = new Child();
c.virtualMtd();    //通过子类变量调用虚方法
p = c;
p.virtualMtd();    //再次通过基类变量调用虚方法

编译生成程序集,使用ildasm工具查看生成的IL指令,可以看到:

可以看到虽然c#源程序中的三局代码不一样, 但是生成IL的指令语句都是一样的,这就是多态特性的关键所在。

通过往计算堆栈中压入不同的对象引用,三条一样的callvirt指令将调用不同类型的同名方法,这是虚方法调用的实质。

查看基类virtualMtd方法的元数据(在ildasm的视图->元数据->显示)

可以看到c#编译器为定义为virtual的方法添加了一个引人注目的”【NewSlot】“标记,这就意味着将在Parent类的方法表中新加一行。再看子类的virtualMtd方法的元数据

可以看到一个醒目的”【ReuseSlot】“标记,这说明子类类型的方法表中也有一个virtualMtd方法,而且它在方法表中的位置与基类中的virtualMtd方法在基类方法表中的位置一样。如下图

如图中所示,不管是基类还是子类,virtualMtd方法在方法表中的索引值都是5(Object类的ToString等方法占用方法表的前四行,构造函数又占用一行),从示例代码生成的IL指令可知虚方法调用时三条语句都生成以下IL指令:

 IL_0008:  callvirt   instance void OnlyYou6.Parent::virtualMtd()

  CLR在执行callvirt指令前,会把要调用的对象引用压入计算堆栈中,这一对象所关联的类型方法表将被用于查找真正调用的方法。

  CLR执行callvirt指令时,它在Parent类型表查找virtualMtd方法,得到一个索引值5,然后它根据执行此方法的对象(前面压入计算堆栈的那个对象引用)的真实类型查对应的方法表(若当前对象为Child类型,则查找Child方法表),用前面得到的索引值5到方法表中找到应该调用的方法。(即Child::virtualMtd方法)

基于接口的虚方法调用

前面介绍的基于继承的虚方法调用原理也适用于接口多态的情况。c#中一个类只能有一个基类,但却可以实现多个接口,正是这个原因,使得基于接口的虚方法调用比基于继承的复杂,要用到接口的方法表和类的方法表。

示例代码如下:

namespace InterfaceVirutalMethodInvoke
{
    interface IOne
    {
        void f1();
        void f2();
    }
    interface IOther
    {
        void g();
    }

    class MyClass1 : IOne, IOther
    {
        void IOne.f1()
        {
        }
        void IOne.f2()
        {
        }
        void IOther.g()
        {
        }
        //自己定义的其他方法
        public void MyOtherMethod()
        {
        }
    }
    class MyClass2 : IOne, IOther
    {
       
        void IOne.f1()
        {
        }

        void IOne.f2()
        {
        }

        void IOther.g()
        {
        }
        public void DoSomething()
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            InvokeIOtherMethod(new MyClass1());
            InvokeIOtherMethod(new MyClass2());
        }
        //基于IOther接口调用虚拟方法
        static void InvokeIOtherMethod(IOther obj)
        {
            obj.g();
        }
    }
}

编译生成程序集,利用ildasm工具查看生成的IL指令

对比IL_0006和IL_0011两句,不难发现c#编译器生成的方法调用指令都是一样的,只是作为参数传入的对象类型不一样,第一个是MyClass1,第二个是MyClass2.

再看下InvokeIOtherMethod方法的IL指令代码

这意味着InvokeIOtherMethod方法的两次调用将会调用不同对象的g方法。

再查下IOther接口g方法的元数据:

可以看到接口中的方法都是公有的虚方法,而且必须在类型方法表的虚方法区域增加一行(NewSlot)。
对比”显示“实现IOther接口的MyClass1的g方法元数据:

 可以看到原来接口中的公有方法变成了私有的虚方法,而且不允许继承,另外,同样有一个NewSlot标记

  为了实现基于接口的虚方法调用,CLR采用了一种复杂的接口方法调用机制,以上文中的MyClass1为例

MyClass1实现了接口IOne, IOther,因此它的接口偏移表中有两个对应的表项,并且其索引值指明了实现两个接口的第一个实例方法的索引。比如MyClass1实现的IOne接口在方法表中的索引值是4,IOther为6。

当CLR执行InvokeIOtherMethod方法内的以下虚方法调用指令时:

IL_0002:  callvirt   instance void InterfaceVirutalMethodInvoke.IOther::g()

  CLR先查IOther方法表,知道其中g方法的方法表索引值为0,再查调用此方法的对象MyClass1所对应的接口偏移表,知道IOther接口的对应方法在MyClass1方法表中的偏移为6,两者相加(6+0 = 6),CLR就知道MyClass1表的第7行(索引值从0开始)是真正需要调用的方法。

这个过程看上去相当复杂,所以相同的功能分别采用继承和接口多态两种方式实现,估计基于接口的多态代码会比基于继承的代码执行起来慢一些。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值