也谈.net下面的new、virtual和override(一)

貌似这几个关键字一直很热,我也凑个热闹,谈一谈,加深一下理解。

先看这两个class
class A
{
 public void T()
 {
  Console.WriteLine("A");
 }
}

class B : A
{
 public new void T()
 {
  Console.WriteLine("B");
 }
}

A a1 = new A();
A a2 = new B();
B b1 = new B();
b b2 = (B)a2;

a1.T();
a2.T();
b1.T();
b2.T();

执行上面的代码,结果如下:
A
A
B
B

第一个A好解释,因为就是调用的A的T方法。第一个B也好解释,是调用的B的T方法。
第二个A呢?为了看清楚事情的本质,我们只好去看下汇编代码了。为了方便,我这里就写一些伪汇编代码了,其中只摘录了主要的部分,省略了一些不必要的细节。

a1.T():
mov ecx,XXX --XXX代表某个地址
call FCFA8618 --A的T方法的具体地址

a2.T():
mov ecx,XXX --XXX代表某个地址
call FCFA8618 --A的T方法的具体地址

可以看到,调用T方法的时候,指向的是同一个地址。也就是说,虽然a2在内存里面指向的对象,本质上是B,但是因为a2是A类型的,所以调用的就是A::T()。当名字相同的时候(有两个T方法),类型起到了决定作用。

继续看:
b1.T():
mov ecx,XXX --XXX代表某个地址
call FCFA86A0 --B的T方法的具体地址,注意这个地址已经和上面的不一样了。

b2.T():
mov ecx,XXX --XXX代表某个地址
call FCFA86A0 --B的T方法的具体地址。

可以看到,b1和b2调用的都是B类的T方法。也就是说,对于实方法(就是没有用virtual修饰的方法),在编译之后,都会有一个固定的位置。调用的时候,直接去调用就可以了。这样实现也不难理解,毕竟,一切都是确定的,类型是确定的,方法是确定的,直接调用,当然是最合适的了。同时,b2也再次说明了,当方法名字一样的时候,类型起作用。

由此,我们也可以反向推出A和B的方法表。
A的方法表:
名字T,地址FCFA8618
名字X,地址XXX
…………

B的方法表:
名字T,地址FCFA86A0
名字A::T,地址FCFA8618
…………
注意B里面肯定是有A的T方法的,否则当我们用一个A类型去引用B的实例的时候,就没有办法调用A的T方法了。只是A的T方法,不能用B去调用而已。

我再补充一点,如果B里面不写new的话,编译器会为我们自动加上,同时会给出一个警告。

上面说完了new关键字,下面说virtual和override。
class C
{
 public virtual void T()
 {
  Console.WriteLine("C");
 }
}

class D : C
{
 public override void T()
 {
  Console.WriteLine("D");
 }
}

C c1 = new C();
C c2 = new D();
D d1 = new D();
D d2 = (D)c2;

c1.T();
c2.T();
d1.T();
d2.T();

执行上面的代码,结果如下:
C
D
D
D

第一个C和第二个D,就不解释了。为了解释清楚另外两个D,还是要看下汇编。
c1.T():
mov ecx,XXX --XXX代表某个地址
call XXX+38h --C的T方法的具体地址,是计算出来的

c2.T():
mov ecx,XXX --XXX代表某个地址
call XXX+38h --D的T方法的具体地址,是计算出来的,而且可以看到,偏移量一样,都是38h

d1.T():
mov ecx,XXX --XXX代表某个地址
call XXX+38h --D的T方法的具体地址,偏移量也是38h

d2.T():
mov ecx,XXX --XXX代表某个地址
call XXX+38h --D的T方法的具体地址,偏移量也是38h

可以看出来,无论是调用C还是D的T方法,都是在某个地址的位置上加上了一个相同的偏移量。所以,当调用c2的T方法时,虽然c2是C类型的,但是指向的还是一个D类型的实例,而且在调用T方法的时候,不是call了一个具体的地址,而是算出来了一个地址。因为是D类型,计算的时候是以D为基准的,所以算出来的就是D的T方法,而不是C的。

也就是说,通过virtual和override,我们定义了一个契约,用virtual修饰的方法,都会放在某个相对位置,并且后面的子类如果使用了override关键字,那子类的方法也会放在同样的相对位置。这样,我们就可以在子类里面实现各自的方法,只要遵守这个约定,方法就会放到同一个相对位置。这样,将来被调用的时候,就能够被灵活的调用到不同的方法了。

我再习惯性的补充一下:如果D里面没用使用override,那就跟B一样了。大家可以试试看。

下面,我来归纳一下由此反推出来的C和D的方法表:
C的方法表:
名字T,地址 某个位置偏移38h
名字X,地址XXX
…………

D的方法表:
名字T,地址 某个位置偏移38h
名字X,地址XXX
…………


当然了,得到了灵活性,必然要损失一些性能,就损失在了call的时候地址的计算上面了。那这个损失到底有多大呢?试试看吧。
我把T方法改一下,如下:
T()
{
 i++;//因为打印太浪费时间了,不适合测试用
}

下面我分别调用a1.T、c1.T和d1.T各一千万次,并且重复三遍,看看时间上面有多少区别。其中a1代表实方法,c1代表父类的虚方法,d1代表子类的虚方法。从理论上来说,应该是a1最快,c1和d1一样的。(不应该认为基类会比子类快,因为上面的汇编已经表明,基类和子类都是同样的寻址方式。)
下面是我的测试结果,数字的单位是ticks数:
a:1406250
a:1250000
a:1250000

c:1250000
c:1406250
c:1406250

d:1406250
d:1406250
d:1250000

平均来看,确实a最快,c和d一样。不过,a也快得有限。所以,为了灵活性,我们还是可以放心使用虚函数的,只要不滥用就可以了。

上面主要讲了三个关键字。其实,这里面还有不少细节。就这样错过很可惜。在下一篇里面,我会谈谈这些细节的问题,里面会说到堆和栈的调用,详细了解一下.net运行时(CLR)到底是如何执行我们写的代码的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值