你不可不知的“继承”
学习面向对象,一直会和继承打交道,也就是说不理解继承就等于没学面向对象一样。以前学习继承,看上去明白了,可是一想还是不明白,知道今天学习了IL后,才感觉明白继承了。在C#的代码
运行结果是
这是个最简单的继承的例子。看下他们的IL代码
你会发现它们的调用所生成的IL代码都一个模式 ,唯一的区别是名字的不同,这就让人想 是什么让这段看似相同的代码产生不同的结果呢?
带着疑问继续追踪,逐个看下各个方法的IL代码是什么样的,于是就有了一下的区别:
这是父类用的是 public void DoWork()
这是子类中 public new void DoWork()
这是父类中的 Public virtual void DoVirtualWork()
这是子类的public new virtual void DoVirtualWork()
这是父类的 public virtual void DoVirtualAll()
这是子类的 public override void DoVirtualAll()
看了比较后总结出,父类是普通的方法,子类用new重写,那么它们的代码转成IL是基本上是一样的,父类用virtual 方法,子类用new virtual 方法复写,它们的IL代码也和第一个一样,基本上是与父类的相同(上图中的不同是因为加了句base,DoVirtualWork())。最后看父类用virtual 方法,子类用override 方法复写.,两者生成的IL代码就不同了,而恰好也是在运行后,显示结果不同的一对方法。
在仔细比较了发现DoVirtualWork 的两个方法的IL代码之后发现了其中的不同之处:在父类中方法前面的修饰用的是newslot ,而子类中用的确是 override ,关于两者的差别查了MSDN ,上面的解释是:
newslot 或 override 定义成员如何与具有相同签名的继承成员进行交互: newslot 隐藏具有相同签名的继承成员。 override 替换继承的虚方法的定义。默认为 newslot。 |
那么通过这段关于描述,我们可以粗略的知道两个DoVirtualWork方法显示的结果为什么不一样了。
但是我们虽然知道这是导致他们不同的原因,但是我们还是不知道为何会调用这两个方法,看Main方法中的IL代码,他们在Main里转成的调用IL代码都是相同的,而且它们都是显示的是callvirt instance void Inheir.Father::DoVirtualWork() 。这又是为何调用不同的呢?
按照当前的证据,那么只能是 callvirt 的问题了。于是找到MSDN中关于callvirt 的解释,如下:
堆栈转换行为依次为: 1.将对象引用 obj 推送到堆栈上。 2.将从 arg1 到 argN 的方法参数推送到堆栈上。 3.从堆栈中弹出从 arg1 到 argN 的方法参数和对象引用 obj;通过这些参数执行方法调用并将控制转移到由方法元数据标记引用的 obj 中的方法。完成后,被调用方方法生成返回值并将其发送给调用方。 4.将返回值推送到堆栈上。 callvirt 指令对对象调用后期绑定方法。也就是说,基于 obj 的运行时类型而不是在方法指针中可见的编译时类型选择方法。Callvirt 可用于调用虚方法和实例方法。tail 前缀可以紧靠 callvirt 指令之前 (Tailcall),以指定在转移控制前应释放当前堆栈帧。如果调用将控制转移到比原始方法信任级别更高的方法,将不释放堆栈帧。 方法元数据标记提供要调用的方法的名称、类和签名。与 obj 关联的类是属于实例的类。如果该类定义匹配指示的方法名称和签名的非静态方法,则调用该方法。否则按顺序检查此类的基类链中的所有类。如果未找到任何方法,则是错误的。 Callvirt 在调用方法前从计算堆栈中弹出该对象和关联的参数。如果该方法具有返回值,则在方法完成后将该返回值推送到堆栈上。在被调用方,obj 参数被作为参数 0 访问,arg1 被作为参数 1 访问,依此类推。 将参数按从左到右的顺序放到堆栈上。即,先计算第一个参数并将其放到堆栈上,然后处理第二个参数,接着处理第三个参数,直到将所有需要的参数都按降序放置在堆栈的顶部为止。必须在任何用户可见参数前推送实例引用 obj(callvirt 始终需要的)。签名(在元数据标记中携带)不需要在参数列表中为该指针包含项。 |
|
看了MSDN的解释,把自己搞的更晕了,接着搜了篇文章,他也对类似的这个例子做了解释.。
网友redpeachsix的文章中的引用:
* 对于非虚方法编译为IL时候, *编译为,找到离编译时所能知道的对象类型最近的并且定义过这个函数的class,定义为
<离编译时所能知道的对象类型最近的并且定义过这个函数的class>.<Function> *如果使用this调用,使用call; 如果使用instance调用,使用callvirt
(因为instance可能会抛nullreferenceexception,而this永远不会) * 运行时,如果是call,直接调用这个<找到离编译时所能知道的对象类型最近的并且定义过这个函数的class>.<Function> * 但如果是callvirt,会到虚函数表中查找(其实多此一举),什么也不会有,然后仍然调用<找到离编译时所能知道的对象类型最近的并且定义过这个函数的class>.<Function> * * 对虚方法编译为IL时候, *编译为,找到离编译时所能知道的对象类型最远的并且定义过这个函数的class,定义为
<离编译时所能知道的对象类型最远的并且定义过这个函数的class>.<Function> * 一般使用callvirt,除非base.虚方法(),
值类型.虚方法(),
Sealed Instance.虚方法() *然后,在运行时,知道对象的实际类型,再根据虚函数表,调用离实际类型最近的,并且override这个函数的class,然后调用这个override的Function
*对于上述几点,注意关键字new的阻断作用 |
读了他的这几段文字后,感觉豁然开朗了,原来对于虚方法的调用还有这个说法。这时整个思路就很清楚了,也就知道为什么会这样了。
那么我们的例子中的调用就是,它先找虚函数表,找到了DoVirtualAll函数,再调用离实际类型(这里都是Father类型),而后一步是关键,它还会找override这个函数的class ,那么对于son,它就是因为找到了override 才调用了这个override的Function。所以出现了这个结果。再回头想下前面的几个,第一个因为是非虚函数,就用的是离最近的方法,直接找的是Father的方法。而第二个虽然是虚函数,但是因为没有override 所以也就没又调用到son的DoVirtualWork方法。
通过这个例子的分析,也就明白函数继承的调用原理了。