c#/Unity中new override virtual的底层IL实现及区别

本文为原创文章,禁止一切形式的转载

首先我们创建一个用于被继承的类 Mira1:

class Mira1
{
    public virtual void f1()
    {
        Debug.Log("parent f1");
    }

    public void f2()
    {
        Debug.Log("parent f2");
    }
}

然后创建 Mira1 的子类 Mira2:

class Mira2 : Mira1
{
    public void f1()
    {
        Debug.Log("child f1");
    }

    public void f2()
    {
        Debug.Log("child f2");
    }
}

Mira1在编译之后,反编译它的两个函数获得的IL代码如下:

mira1

可以看到,加上virtual后IL指令中只是多出了newslot和virtual两个指令

virtual用于声明该函数是虚函数

newslot需要和virtual一同使用,该指令让虚函数不会override基类中相同签名的虚方法,而是在虚函数表中创建一个新的slot。

听起来有点晕?没事,我们先解释一下虚函数表。

虚函数表(Virtual Method Table,或v-table、dispatch table)在许多支持【多态】特性的语言中出现。

当定义一个虚函数时,编译器会在类中添加一个隐藏的指针变量,指向一张虚函数表。虚函数表有多个slot,每个slot存放一个虚函数的地址。在运行中调用某个虚函数时便会在虚函数表中查找相应函数的地址然后进行调用。也就是说虚函数表是用于实现多态的特性的。

我们先看一下在子类的f1函数添加new和override分别是什么效果:

添加new:

public new void f1()

iL代码:

.method public hidebysig instance void  f1() cil managed

对比上面基类f2的iL指令,好像没什么区别?


添加override:

public override void f1()

iL代码:

.method public hidebysig virtual instance void f1() cil managed

很明显,仅是多了一个virtual


看到这里,就可以解释这些指令分别是什么意思了:

hidebysig

即hide by signature,通过签名隐藏。与之相对的是“hide by name”。通过签名隐藏指在派生类中隐藏基类相同签名的方法。如果去掉这个标识,那么函数将会变成“hide by name”模式,即使函数签名不同,基类同名的函数也会被隐藏。比如基类有一个foo(),派生类有一个foo(int)方法,在hidebysig模式下子类实例依然可以调用foo(),但hide by name模式下调用foo()会报错。在国内很多博客中都把hidebysig误传为new关键字,怎么说呢首先功能有一些相似,但你会发现override或virtual声明的函数也有这个指令,而且去掉它也不是去掉new的功能,我觉得需要更正一下。


virtual

声明一个函数为虚函数,函数将会被添加到虚函数表中


newslot

与virtual搭配使用,让虚函数不会覆盖基类中相同签名的虚方法。是不是觉得有点矛盾?但它正是用来实现这么矛盾的功能的。

需要注意一下,C#和C++的virtual和override有些区别,在C++中override是可以不写的,子类相同签名的函数会覆盖基类中的虚函数。不管子类是virtual还是override还是没有显式声明。而在c#中必须显式的标明override,即override的功能是让函数作为虚函数并覆盖基类同签名的方法,如果派生类的函数标记为virtual它将成为虚函数并隐藏基类中的虚函数。如果什么都没写就等于new关键字。

所以:

  • 如果函数声明是override,iL代码中会标记为virtual,函数地址放进虚函数表,如果有基类的同签名函数的话,函数地址直接覆盖过去

  • 如果函数声明是virtual,iL代码中将会标记为virtual newslot。函数地址放进虚函数表,如果有基类的同签名函数的话,不覆盖,而是新开一个slot。如果用基类指针调用子类的实例,运行的函数是基类的,效果相当于隐藏了(两个虚函数共存)


其他指令:
  • .method:方法声明
  • public:不用解释
  • instance:成员函数,于static相对
  • void:函数的返回类型
  • cil:comman intermediate language协议
  • managed:托管代码,与非托管代码相对

从上面添加new后的代码结合Unity的warning提示,我们可以得知new是缺省的,如果函数没有override会默认为new

warning


单继承的情况

要想实现多态,必须virtual和override搭配使用,基类函数没有virtual声明时,子类无法override,此时子类隐藏父类同签名的函数。基类函数是virtual虚函数时,子类需要显式声明override,否则默认为new,或手动添加new标记。

以下代码:

class Mira1
{
    public virtual void f1()
    {
        Debug.Log("parent f1");
    }
}

class Mira2 : Mira1
{
    public void f1()
    {
        Debug.Log("child f1");
    }
}
Mira1 m1 = new Mira2();
m1.f1();

输出结果是:

parent f1

子类的 f1 隐藏了基类的函数,并没有覆盖虚函数表的地址。此时对象m1的虚函数表有两个函数地址,分别是基类和子类的 f1 。当用基类的指针指向子类的实例,调用的还是基类的函数。


当子类改为:

class Mira2 : Mira1
{
    public override void f1()
    {
        Debug.Log("child f1");
    }
}

运行结果便是子类的"child f1",m1的虚函数表中只有一个函数地址,子类的f1函数地址覆盖了基类的函数地址。


多重继承的情况

接下来看一个绕一点的:(注意 virtual 和 override)

class A
{
    public virtual void f()
    {
        Debug.Log("A: f");
    }
}

class B : A
{
    public virtual void f()
    {
        Debug.Log("B: f");
    }
}

class C : B
{
    public override void f()
    {
        base.f();
        Debug.Log("C: f");
    }
}

你觉得以下的代码会输出什么?

A m = new C();
m.f();

答案是

A: f

通过iL代码可以看到,调用的仅是A的函数:

abc_vvo

B类的f并没有覆盖A,而C类覆盖的是B类的f(A类的已被B类的f隐藏)

slot

如果把B类的f()也写成override,那么输出就是

B: f
C: f

这时虚函数表只有C: f 一项了。

此时下面代码:

A m = new C();
m.f();

编译成iL后调用f() 的指令还是:

callvirt   instance void A::f()

只不过这时A::f() 指向的虚函数表的地址已经变成了C::f() 的地址。


你可能会问,既然C::f() 已经把 B::f() 覆盖了,那么为什么还可以通过base.f(); 调用B::f() ?

因为此时不是通过callvirt指令调用,而是call指令,base.f()编译后变成了:

call       instance void B::f()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值