• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

C#的虚函数解析机制

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

前言

  这篇文章出自我个人对C#虚函数特性的研究和理解,未参考、查阅第三方资料,因此很可能存在谬误之处。我在这里只是为了将我的理解呈现给大家,也希望大家在看到我犯了错误后告诉我。

用词约定

  • “方法的签名”包括返回类型、方法名、参数列表,这三者共同标识了一个方法。
  • “声明方法”,即指出该方法的签名。“定义方法”,则是指定调用方法时执行的代码。
  • “同名方法”是指方法的签名相同的两个方法。
  • “重写”一个方法,意味着子类想继承父类对方法的声明,却想重新定义该方法。
  • 单独使用“使用”一词时,包括“显式”或“隐式”两种使用方式:前者是指在代码中指明,后者是根据语句的上下文推断。
  • 某个类的方法,包括了在该类中定义的方法,以及由继承得到的直接父类的方法。注意这条规则的递归性质。  

理论部分

  在父类与子类里,除了类之间的继承链,还存在方法之间的继承链。

  C#里,在一个类中声明一个方法时,有四个和方法的继承性有关的关键字:newvirtualsealedoverride

  • virtual 表示允许子类的同名方法与其建立继承链。
  • override 表示其与父类的同名方法之间建立了继承链,并隐式使用 virtual 关键字。
  • new 表示其切断了其与父类的同名方法之间的继承链。
  • sealed 表示将其与父类的同名方法建立继承链(注意这个就是 override 关键字的特性),并且不允许子类的同名方法与其建立继承链。在使用 sealed 关键字时,必须同时显式使用 override 关键字。

  以及:

  • 在定义方法时,若不使用以上关键字,方法就会具有new关键字的特性。对于这一点,如果父类中没有同名方法,则没有任何影响;如果父类中存在一个同名方法,编译器会给出一个警告,询问你是否是想隐藏父类的同名方法,并推荐你显式地为其指定new关键字。

  ①其:指代正在进行声明的方法。

 

  依照上述的说明,在调用类上的某个方法时,可以为该方法构建出一个或多个“方法继承链”。首先列出从子类一直到父类的类继承链,并列出这些类对该方法的最初定义或重定义。然后从父类到子类,逐个检查每个类对该方法的定义,按以下规则构造方法继承链:

  1. 任何一个没有使用 overridesealed 关键字的方法定义都将成为继承链的开端;
  2. 如果该类在定义方法时使用了 virtual 关键字,则会被附加到继承链中。
  3. 继承链的结束取决于两个因素:若子类中存在使用了 new 关键字的同名方法,则之前的继承链立刻结束(该方法不会被添加到继承链中);若子类中存在使用了 sealed 关键字的同名方法,则在将该方法添加到继承链后,然后结束继承链。

  当你拿到一个子类的实例,却使用父类的对象引用调用一个方法时(例如“A instanceRef = new C(); instanceRef.Foo1()”,这时类型A的引用就指向了类型C的对象),C#会先检查该方法是否为一个虚方法(使用了 virtual 关键字):如果不是,则简单地调用该方法的父类版本即可;如果是,则沿着方法的继承链向下寻找,找到位于继承链底部的那个方法。
  ②子类:指该实例的实际类型。
  ③父类:指在调用方法时,使用的对象引用的类型;该类型必然是子类的父类型。

实践部分

  我定义了以下四个类:

  1. public class A
  2. {
  3.     public virtual void Foo1()
  4.     {
  5.         Console.WriteLine("A.Foo1() was invoked.");
  6.     }
  7.     public void Foo2()
  8.     {
  9.         Console.WriteLine("A.Foo2() was invoked.");
  10.     }
  11. }
  12. public class B : A
  13. {
  14.     public override void Foo1()
  15.     {
  16.         Console.WriteLine("B.Foo1() was invoked.");
  17.     }
  18.     public new virtual void Foo2()
  19.     {
  20.         Console.WriteLine("B.Foo2() was invoked");
  21.     }
  22. }
  23. public class C : B
  24. {
  25.     public new void Foo1()
  26.     {
  27.         Console.WriteLine("C.Foo1() was invoked.");
  28.     }
  29. }
  30. public class D : C
  31. {
  32.     public override sealed void Foo2()
  33.     {
  34.         Console.WriteLine("D.Foo2() was invoked.");
  35.     }
  36. }

  当运行如下代码时,会打印出什么?

  1. C aD = new D();
  2. A aC = new C();
  3. aD.Foo1();
  4. aD.Foo2();
  5. aC.Foo1();
  6. aC.Foo2();

 结果是:
C.Foo1() was invoked.
D.Foo2() was invoked.
B.Foo1() was invoked.
A.Foo2() was invoked.

 

  例子很简单,依照之前的规则,可以画出如下一幅图。图中圆形的末端表示封闭、中断继承链;菱形的末端表示开放、允许构建继承链;类描述中的等式,表示从该类型的对象引用调用对应方法(等号左边的斜体)时,实际执行的代码体是在何处(等号右边的正常字体)定义的。

                    其实,为了确认这里描述出来的方法的继承链,甚至都不需要实地运行此代码。将代码放在Visual Studio里,使用“重构”(Refactor)菜单中的“重命名”(Rename)修改方法名称,待完成后就会发现在方法继承链的中断处,自动修改符号名称的动作也中止了。

补充

  对于 this 关键字,上述的规则也适用。只需要将 this 依照当前的代码上下文翻译为对应的类型引用,就可以依照之前叙述的方法确定最终调用的代码了。例如在C中的Foo1方法里假如有这么一条语句:“this.Foo2()”。当在外部运行“D.Foo2()”时,就会就会解析到“C.Foo1()”,这时,C.Foo1()方法的内部在解析“this.Foo2()”时就会解析到D.Foo2()。

  对于 base 关键字,则比较简单,只是在基类的方法(这里“基类的方法”一词,请参见“用词约定”的第6条。)中找到同名方法,然后调用,不存在解析虚函数的过程。

  对于被委托对象包装的方法指针,在调用委托时,仍会按照上述规则解析到正确的方法。

  本文示例中使用了“Foo”开头的方法名,而这个习惯借鉴自一些别的文章。这里是有个典故还是怎么?

转自:http://www.cnblogs.com/560889223/archive/2008/12/03/1346340.html


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap