[你必须知道的.NET]
第十五回:继承本质论
发布日期:2007.9.10 作者:Anytao
©2007 Anytao.com ,原创作品,转贴请注明作者和出处。
本文将介绍以下内容:
1. 引言
关于继承,你是否驾熟就轻,关于继承,你是否了如指掌。
本文不讨论继承的基本概念,我们回归本质,从编译器运行的角度来揭示.NET继承中的运行本源,来发现子类对象是如何实现了对父类成员与方法的继承,以最为简陋的示例来揭示继承的实质,阐述继承机制是如何被执行的,这对于更好的理解继承,是必要且必然的。
2. 分析
下面首先以一个简单的动物继承体系为例,来进行说明:
public abstract class Animal
{
public abstract void ShowType();
public void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
public class Bird: Animal
{
private string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
private string color;
public string Color
{
get { return color; }
set { color = value; }
}
}
public class Chicken : Bird
{
private string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public void ShowColor()
{
Console.WriteLine("Color is {0}", Color);
}
}
然后,在测试类中创建各个类对象,由于Animal为抽象类,我们只创建Bird对象和Chicken对象。
public class TestInheritance
{
public static void Main()
{
Bird bird = new Bird();
Chicken chicken = new Chicken();
}
}
下面我们从编译角度对这一简单的继承示例进行深入分析,从而了解.NET内部是如何实现我们强调的继承机制。
(1)我们简要的分析一下对象的创建过程:
Bird animal = new Bird();
Bird bird创建的是一个Bird类型的引用,而new Bird()完成的是创建Bird对象,分配内存空间和初始化操作,然后将这个对象赋给bird引用,也就是建立bird引用与Bird对象的关联。
(2)我们从继承的角度来分析在编译器编译期是如何执行对象的创建过程,因为继承的本质就体现于对象的创建过程。
在此我们以Chicken对象的创建为例,首先是字段,对象一经创建,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止。我们可以在编译器中单步执行的方法来大致了解其分配的过程和顺序,因此,对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的存储顺序是由上到下排列,object类的字段排在最前面,其原因是如果父类和子类出现了同名字段,则在子类对象创建时,编译器会自动认为这是两个不同的字段而加以区别。
然后,是方法表的创建,必须明确的一点是方法表的创建是类第一次加载到CLR时完成的,在对象创建时只是将其附加成员TypeHandle指向方法列表在Loader Heap上的地址,将对象与其动态方法列表相关联起来,因此方法表是先于对象而存在的。类似于字段的创建过程,方法表的创建也是父类在先子类在后,原因是显而易见的,类Chicken生成方法列表时,首先将Bird的所有方法拷贝一份,然后和Chicken本身的方法列表做以对比,如果有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。这种创建过程也是逐层递归到Object类,并且方法列表中也是按照顺序排列的,父类在前子类在后,其原因和字段大同小异,留待读者自己体味。
结合我们的分析过程,现在将对象创建的过程以简单的图例来揭示其在内存中的分配情形,如下:
从我们的分析,和上面的对象创建过程可见,对继承的本质我们有了更明确的认识,对于以下的问题就有了清晰明白的答案:
- 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法。
- 子类可以调用父类方法和字段,而父类不能调用子类方法和字段。
- 虚方法如何实现覆写操作,使得父类指针可以指向子类对象成员。
- new关键字在虚方法继承中的阻断作用。
你是否已经找到了理解继承、理解动态编译的不二法门。
3. 思考
通过上面的讲述与分析,我们基本上对.NET在编译期的实现原理有了大致的了解,但是还有以下的问题,一定会引起一定的疑惑,那就是:
Bird bird2 = new Chicken();
这种情况下,bird2.ShowType应该返回什么值呢?而bird2.type有该是什么值呢?有两个原则,是.NET专门用于解决这一问题的:
- 关注对象原则:调用子类还是父类的方法,取决于创建的对象是子类对象还是父类对象,而不是它的引用类型。例如Bird bird2 = new Chicken()时,我们关注的是其创建对象为Chicken类型,因此子类将继承父类的字段和方法,或者覆写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型不同的区别决定了不同的对象在方法表中不同的访问权限。
注意
根据关注对象原则,那么下面的两种情况又该如何区别呢?
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();
根据我们上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内进行执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题。
- 执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类编译的原因了。
思考
1. 上面我们分析到bird2.type的值是“Bird”,那么bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据本文上面的分析,想想到底为什么?
2. 关于new关键字在虚方法动态调用中的阻断作用,也有了更明确的理论基础。在子类方法中,如果标记new关键字,则意味着隐藏基类实现,其实就是创建了与父类同名的另一个方法,在编译中这两个方法处于动态方法表的不同地址位置,父类方法排在前面,子类方法排在后面。
在.NET中,如果创建一个类,则该类总是在继承。这缘于.NET的面向对象特性,所有的类型都最终继承自共同的根System.Object类。可见,继承是.NET运行机制的基础技术之一,一切皆为对象,一切皆于继承。本文从基础出发,深入本质探索本源,分析疑难比较鉴别。对于什么是继承这个话题,希望每个人能从中寻求自己的答案,理解继承、关注封装、玩转多态是理解面向对象的起点,希望本文是这一旅程的起点。
[祝福]
仅以此篇献给我的老师们:汤文海老师,陈桦老师。
参考文献
(USA)Don Box, Essential .NET
(中国)虫虫,从编译的角度看对象
温故知新
[开篇有益]
[第一回:恩怨情仇:is和as]
[第二回:对抽象编程:接口和抽象类]
[第三回:历史纠葛:特性和属性]
[第四回:后来居上:class和struct]
[第五回:深入浅出关键字---把new说透]
[第六回:深入浅出关键字---base和this]
[第七回:品味类型---从通用类型系统开始]
[第八回:品味类型---值类型与引用类型(上)-内存有理]
[第九回:品味类型---值类型与引用类型(中)-规则无边]
[第十回:品味类型---值类型与引用类型(下)-应用征途]
[第十一回:参数之惑---传递的艺术(上)]
[第十二回:参数之惑---传递的艺术(下)]
[第十三回:从Hello, world开始认识IL]
[第十四回:认识IL代码---从开始到现在]
©2007 Anytao.com
原创作品,转贴请注明作者和出处,留此信息。
本贴子以“现状”提供且没有任何担保,同时也没有授予任何权利。
This posting is provided "AS IS" with no warranties, and confers no rights.
posted @ 2007-09-10 21:43
Anytao 阅读(12371)
评论(113) 编辑 收藏
发表评论
继承是面向对象的特征,本人认为继承的精髓在于它可以解决什么问题,而不是楼主介绍的在.net里的实现机制。因为实现机制都是变化的,因公司,因人的喜好不同,实现机制也有不同(比如c#和java的不同)。感觉有点吹毛求疵,南辕北辙了。特别说明,我仅仅是发表我对这篇文章的看法,没有别的意思,不要误会。
@thh
父类当然不可调用子类方法,因为父类对象中根本没有子类方法,关于虚函数动态调用正确的说法是:父类指针可以调用子类对象。在动态调用时,父类还是调用其本身的方法,只不过这个方法被子类方法覆写罢了,但是在概念上和“父类可以调用子类方法”完全是两回事。
关于基础只说,只能是仁者见仁了,:-)。
@Anders Cui
呵呵,的确如此,关于构造函数的创建很有必要加以论述,可能在系列的后续中有所涉猎,敬请关注。
@today
感谢你的意见。
我认为了解其运行机制和了解其应用方向是两个必不可少的方面,缺一不可。从内存的布局、分配,我们就可以很清楚也很深刻的了解动态调用的实现原理,从而对继承、对多态,包括面向对象都有更好的了解。
关于Java和.NET的实现机制,至少在这一块也都大同小异,有区别但不大。
我始终认为从本质入手来了解技术,是为应用技术做了最好的铺垫。
一家之言,仅供参考。谢谢。
Anytao说:关于Java和.NET的实现机制,其实也都大同小异,有区别但不大。java默认所有的父类方法是虚的,而C#不是虚的,这一点的区别还是挺大的。today说:实现机制都是变化的,java或c#继承的实现机制还会有可能变化吗?你说因公司,因人的喜好不同,实现机制也有不同(比如c#和java的不同),这话精典,我是设计者的话,我想怎样规定就怎样规定了:),讨论这个话题应该不是吹毛求疵,南辕北辙吧,你怎么理解Bird bird = new Chicken()?你怎么知道bird.Method()执行的是Bird中的方法还是Chicken中的方法?背下来的?还是另有其他方法来理解这个事情?请today兄不吝指教一下,我认为从编译的角度看对象,是最好的。
@双眼皮
其实,按照Anytao 兄的方法,是学习继承比较好的方法。但这太局限于某种语言了,不同的人发明的面向对象的语言,都会支持继承,但又都会有差异,所以,语言仅仅是工具,而继承隐藏的思想(解决问题的能力)却值得寻味。关于你提到的另外一个问题,我是如何知道的,其实这里的关键不在于怎么知道答案的,而是因为答案就是这样子的,.net的设计者就是这样设计的。如果有需求的话,这里的结果完全可以是调用父类的方法(当然父类必须存在这样的方法)。沿着这种思路,很有可能会出现“反继承”。
当然,我的意思并不是说这篇文章写得不好,因为,我也相信探微才可知著。
@today
反继承的说法,倒是很有新意。还是那句话,各取所需就行了,对继承的讨论本文也远不是终点,所以today兄提到的关于如何继承,怎么继承的这些问题,留待后文吧。
@紫色阴影
?
按照我的理解,子类中继承父类方法正是通过拷贝父类方法到其方法表中,不管是虚方法还是非虚方法,否则子类对象又如何获取父类方法呢?
关于你的那篇文章中的图是怎么生成的,很想知道这一点。看来还值得探讨一下。
当然,这是我的理解,希望有机会讨论:-)
@Anytao
我是用windbg跟踪查看的,图也是在此基础上画的
子类的方法表里面的确不包含父类非虚方法定义
非虚方法和虚方法调用不同,当JIT编译以后,非虚方法调用是先将当前对象压栈,然后直接call方法地址,而不像虚方法调用的时候有查表的过程。
非虚方法可以被内联,如果非虚方法调用也要查找方法表,那是做不到内联的
@紫色阴影
的确值得研究一下,也想到是用WinDB来跟踪看的,我再了解一下,还不是很清楚,这是我期望的收获,受教:-)
anytao兄的分析很入理,我一直以来都是拿来一个东西就用,全不管为什么要 这么用.看了你的系列文章,真的受益啊.
对于底层的东西了解的多了,东西才能用的更有技巧.
@孙会生
谢谢,希望对你有所帮助,继续关注会更多。共同进步。
1+1为什么=2啊?
人从哪里来?死了到哪里去啊??
LZ为什么不去研究呢?
继承.NET规定死了就是这种机制了。你还非得去研究。还号召大家一起来研究。
你研究透了继承的本质能对你提高继承的效率有帮助?对你的设计思想有帮助?这太牵强了吧。9×9乘法表为什么要那么乘呢??
那照你这么说,为什么不直接去研究0101机器码不是更加底层更加有用??
不见得知道了编译器的指针调用后,他的抽象能力就能有多高。
面向对象的世界还去研究底层汇编,干脆做C做汇编不是更好?
面向对象讲的是对对象,对事务,甚至对行业的抽象。
@1+1 ?= 2
所谓志不同,道有异。
人从那里来,又到哪里去,肯定有人专门研究,你不能说他们是疯子。
.NET规定死的东西都不去研究,请问哪些又是活的,有些时候从0和1的角度去看问题并非坏事,从计算机本身的角度来理解计算机才叫体察入微,这和从继承机制角度来理解继承是一回事儿,如果非要问个问什么,那就问自己志在哪里,道在何方?每个人的情况是不一样的,如何只想沉迷于拖拽控件的程度上,谁也拦不住你对个人理想的冲动。
另外,谈到对设计思想的帮助,我自己的体会是有过之而无不及,可能更多的感想你未必理解,单纯以对象、事务、抽象来粉饰面向对象,只能停留在面向对象的光环下放光而已。不知1+1 ?= 2兄是否细读本文,如果是,那就再读一次,看看对继承的理解是否有益;如果不是,那也没有讨论的必要,谢谢。
希望常来,切磋一二。
Anytao 兄的无私风险精神很是让人敬佩。
望Anytao不要理会那些无聊谩骂,坚持创作的勇气。引述Steven Jobs的一段话,共勉之:
......
你们的时间有限,所以不要浪费时间活在别人的生活里。不要被教条所局限--盲从教条就是活在别人思考结果里。不要让别人的意见淹没了你内在的心声。最重要的,永远追随自己内心与直觉的勇气,你的内心与直觉多少已经知道你真正想要成为什么样的人(have the courage to follow your heart and intuition. They somehow already know what you truly want to become),任何其它事物都是次要的。
........
求知若饥,虚心若愚(Stay Hungry, Stay Foolish)。
文中“然后,是方法列表的创建,必须明确的一点是对象的创建是在运行时,而方法列表的创建是在编译时”有误。
方法表也是在运行时创建的,编译时创建的怎么会在托管堆(内存)中呢! 编译时生成的是元数据和IL代码。
在运行时CLR通过查找元数据建立方法表(其实是对象的类型所对应的数据结构), 然后把每个方法条目指向CLR的一个函数(暂叫JitCompiler),当一个方法执行时,比如A(),由于A方法条目指向JitCompiler,所以JitCompiler被调用,JitCompiler函数通过元数据找到A方法的IL代码然后把它翻译成本地CPU指令,这些指令保存在一个动态内存中,然后把A方法条目指向该内存地址,并跳到该内存地址上执行,当A方法第二次被调用时,由于A方法条目已经指向本地CPU指令所在内存地址(而不是JitCompiler函数的内存地址),所以直接执行之。这就是常所的只在第一次执行时才把IL代码翻译成CPU指令。
@海天一鸥
:-)
谢谢你的支持,博客是个接受自由的地方,讨论技术是需要这种自由,:-)
@tongxl
的确有误,谢谢你的指正,我再研究研究然后做以合适的修订,非常感谢,希望你常来斧正,:-)
真的很不错。受益了。
这篇文章看了很多次,
终于算是理解了。
@tt1
文中有个别地方表达有误,可以参考一评论,近期将做以修订,谢谢。
@lbq1221119
呵呵,希望经常来讨论:-)
LZ文章不错,受益不浅。。。
以前有想,不过LZ深入。
赞同LZ观点,只有了解了基础与实质,才能更好的创造好的程序。
@小猴子
很有同感,研究.NET还是离不开多少知道点CLR运行机制,这也是这个系列和CLR团队所期望达到的目标:-)
bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据本文上面的分析,想想到底为什么?
本人愚笨,这个没看懂.....
Bird bird2 = new Chicken();
bird2.ShowType();
我们的分析过程可以这样来展开:
1 bird2对象为一个Bird类型指针,但是指向的是Chicken类型的实例,因此在内存布局上,请注意布局图中chicken方法指针指向的Method Table表;
2 ShowType为Bird类型的一个虚函数,并且在子类Chicken中覆写,也就是说Chicken实例中的ShowType方法已经被覆写为新的实现,因此该方法的返回值为“Type is Chicken”;
3 当bird2调用ShowType方法时,它只能获得被重新覆写过的方法,结果自然是“Type is Chicken”。
另外,需要留意new关键字对父类方法的隐藏作用。
lz您好,想跟你请教一个问题。您文中说“首先将Bird的所有方法拷贝一份,然后和Chicken本身的方法列表做以对比,如果有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。
”既然子类方法覆盖同名的父类方法,那么当base父类的方法时,请问上哪去找父类的方法?谢谢你的解惑。
@孤舟垂钓
其实二者并不矛盾,首先关于子类虚方法覆写父类方法的机制问题,本文已经有了较详细的论述,所以暂不讨论。
我想你主要关心的是base关键字的使用问题。本人另外一篇文章《第六回:深入浅出关键字---base和this 》http://www.cnblogs.com/anytao/archive/2007/05/04/must_net_06.html对此有一定的论述,仅供参考。
简单来说,.NET提供了base关键字来主要完成两件事:一是在派生类中访问基类被覆写的方法;二是在派生类中调用基类的构造函数。这就像提供this关键字来当前实例一样,就是.NET提供的一种机制。
如果要强调base调用父类方法是去哪儿找其父类方法这个问题,可以这样来描述,首先找到父类对象,然后根据父类对象的方法表指针定位到该方法,请注意:上述关于子类覆写发生在子类而非父类,父类方法表中仍然是父类本身的方法,你可以从本文的那张模拟图中找到答案。
有些罗唆,见谅。
@Anytao
非常感谢你的答疑。没想到我上午才发,下午你就很耐性的回复了,很是感动,感觉你是一个很有责任感、对技术很执着的人(申明一下,我不是在拍马屁),好像还是个性情中人,哈哈,不知猜得对不对。很想跟你交个朋友。请原谅我的固执,我还想继续问问题,不要拍砖哦。那个父类对象怎么找?我不是在抬扛,真想把问题弄清楚
@孤舟垂钓
呵呵,别这么说,我只是碰巧对这部分比较感兴趣罢了,自己也是个学习者,在园子里Artech、装配脑袋、idior和老赵都是大牛,给了我很多帮着。也非常感谢你参与讨论,这也是我写文章的初衷。
base关键字有两个基本的功能:
1 调用基类非私有成员;
2 在初始化时与基类通信。
例如有如下代码:
public class A
{
public virtual void M()
{
}
}
public class B: A
{
public override void M()
{
//调用基类成员
base.M();
}
}
在子类B的方法M中通过base.M()调用父类方法,编译器会将上述代码编译为:
call instance void A::M()
这样的形式,也就是说base提供了编译器调用父类方法的机制。
当然,要注意多层继承时对父类方法调用的区别:
1 有覆写存在的情况,则base只能访问其直接继承的父类的方法;
2 没有覆写存在的情况,则base可以访问任意上级父类的方法。
@Anytao
lz,8点还在笔耕,向你学习。惭愧啊,那会我在家做饭呢。谢谢,基本上清楚了
@Anytao
这篇文章的两个问题已经被紫色阴影和tongxl指出来了,别忘了修改呀,要不然会让大家困惑的。
问题一:子类方法表中没有父类的非虚方法
问题二:方法表肯定是运行时创建的,是在运行时当CLR需要JIT某个方法的时候,会把这个方法里面所有还没有加载到堆中的所有Types(的方法表)加载到堆中
@Boler Guo
说得及时,忙起来把这个事儿就忘了,这两天尽快改了,谢谢你的提醒,要不然又是一种罪过。:-)
@Boler Guo
已做修改,感谢紫色阴影、tongxl和Boler Guo
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();
这两个对象区别还是有些模糊,望Anytao指教!!!
@qhDocument
你好,这一点其实不难理解,正如文中所言:
1 bird2实例和chicken在内存中的布局都是一样的,其实就是执行了托管堆中的同一块地址,这一点应该很好理解,因为都是以new Chicken()来创建的。
关于创建过程,你可以参考
http://www.cnblogs.com/anytao/archive/2007/12/07/must_net_19.html
的详细论述。
2 不同点需要关注的是bird2和chicken的类型,一个为Bird,一个为Chicken,所以二者对于new Chicken()创建的实例对象的访问权是不同的,这个由那个附加信息offset来保证。所以二者虽然指向了同一块地址,但是其可以访问的范围是有区别的。
不知这样解释,是否清楚:-)
@qhDocument
打个不是很恰当的比方:假如有父子俩在沙漠里向一片绿洲看去,它们俩看的是同一块地方。但是父亲个子高,会看得多一点,而儿子个子矮,只能看得少一点。而这种情况在继承中刚好相反,子类的访问权限较父类大,所以chicken可以调ShowColor,而bird2不可以调用。子类可以代替父类,但是父类不能代替子类。
@Anytao
关于 Bird b = new Chicken();的问题,好象在面试的时候经常会考到:)
根据你的讲解,我的理解是这样的,请指教:
这种情况,用文中提到的两个原则来解释就好了,
1)关注对象原则,也就是说,这个时候创建的是Chicken对象,我们只关注Chicken的内存分布情况就好了:
字段:(按照顺序)b_type,chicken_type
方法表:chincken的showtype方法覆盖了父类Bird的showType方法,所以此时b.showtype(),执行的就是chincken的showtype方法。
2)执行就近原则
如果要执行b.type,很明显,首先找到的就是b_type,输出即可
@Anytao
下面是一个测试:帮忙看看对否?
using System;
using System.Collections.Generic;
using System.Text;
namespace ClassDemo
{
class Program
{
static void Main(string[] args)
{
Chicken c = new Chicken();
c.ShowType(); //输出Type is chicken
Bird b = new Chicken();
b.ShowType(); //输出Type is chicken
superbird sb = new Chicken();
sb.ShowType(); //输出Type is chicken
Console.WriteLine(sb.type); //输出superbird
}
}
public abstract class Animal
{
public abstract void ShowType();
public void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
//加了一个类,不一定好看,只想说明问题
public class superbird :Animal
{
public string type = "superbird";
public override void ShowType()
{
Console.WriteLine("type is {0}",type);
}
}
public class Bird : superbird
{
public new string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
private string color;
public string Color
{
get { return color; }
set { color = value; }
}
}
public class Chicken : Bird
{
public new string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public void ShowColor()
{
Console.WriteLine("Color is {0}", Color);
}
}
}
@Ivan-Yan
首先谢谢你的讨论,最近很忙没能及时回复,很抱歉:-)
关于Bird b = new Chicken();的讨论很正确。
正如你的测试代码看到的那样,这种方法是经得住考验的,因为从内存角度来看问题,正是最可靠的方式:-)
继承是个重要的话题,从内存角度来理解继承,除了能够清晰的看清一切障眼的法门,能重要的是可以提高对于OO的感悟。
你好:anytao老师
我买书不久,刚看到继承,有点迷惑,问下啊,我在brid 类 和 Chicken类中分别加入一个同名的方法:
public class Brid:Animal
{
public void showI()
{
console.writeline("stay in Brid class");
}
}
public class Chicken:Bird
{
public void showI()
{
console.writeline("stay in Chicken class");
}
}
public static void Mian()
{
Brid brid2 = new Chicken();
brid2.showI();
//显示结果为
stay in Brid class
}
1: 按照你书上的 "关注对象原则" brid2指针应该指向Chicken方法表,可为什么调用的是brid类的showI()方法呢?
@kevinli
你好,老师实在是不敢当。
首先感谢你的悉心研究,关于你的问题,其实没有抓住问题的要点。我在本文中自己创造了两个方法:关注对象原则和执行就近原则。对于继承的分析,按照这种思路是完全没问题的。
一定注意的是,”brid2指针应该指向Chicken方法表“,这是没有错的,但是Chicken方法表中同时存在其父类的方法,至于为什么调用了brid类的showI()方法,则是执行就近原则的解释。
下面,首先关注一下执行就近原则,我想应该能很好的给出关于你的示例的答案。
执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类编译的原因了。
Chicken方法表中同时存在其父类的方法,至于为什么调用了brid类的showI()方法,则是执行就近原则的解释。
这句话,让我明白了问题的关键所在(子类有父类的showI方法也有子类本身的showI方法,但遵照就近原则将调用父类的showI),懂你意思了,问题自然解决了(心理舒坦哦).谢谢你啊!
昨晚看书看到十二点多,刚才又看了所有的讨论,还是对执行就近原则和关注对象原则的应用场合有所疑惑。
------------------------------------------------------------------
对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),
------------------------------------------------------------------
按照执行就近原则,bird2.showType访问的不应该是Bird.showType方法呢?
而根据关注对象原则,bird2指向的是Chicken对象,因此brid2.showType访问的是Chicken.showType才是正确的。
当然,根据最终执行结果我的理解肯定是错误的,但究竟错在哪呢?或许这两个原则使用的对象和场合有所区别?还望Anytao兄指正。谢谢。
@3b阿当
呵呵,其实这一块的确比较难于理解,不过我认为并不需要对“执行就近”和“关注对象”这两个名词弄迷惑,这两个词语算是我自己的创造,其他书上应该看不到这种说法,所以是非“官方”的。本来的目的是想通过两个名词利于理解,反倒让你迷惑了。
具体分析吧:
首先: Bird bird2 = new Chicken();
根据分析可知,bird2是一个指向内存布局为Chicken,但是本身为Bird引用,这就设计到从“关注对象”和“执行就近”两方面来分析。因此,必须重申关于方法表创建过程的描述(来自正文):
然后,是方法表的创建,必须明确的一点是方法表的创建是类第一次加载到CLR时完成的,在对象创建时只是将其附加成员TypeHandle指向方法列表在Loader Heap上的地址,将对象与其动态方法列表相关联起来,因此方法表是先于对象而存在的。类似于字段的创建过程,方法表的创建也是父类在先子类在后,原因是显而易见的,类Chicken生成方法列表时,首先将Bird的所有方法拷贝一份,然后和Chicken本身的方法列表做以对比,如果有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。这种创建过程也是逐层递归到Object类,并且方法列表中也是按照顺序排列的,父类在前子类在后,其原因和字段大同小异,留待读者自己体味。
可见,在创建方法表时,如果该方法实现为虚方法,则子类方法将覆盖父类方法。所以,具体是执行Bird.showType还是Chicken.showType要看具体的内存布局。结合文章的实例和示例,我想你能得道准确的答案,不必局限于这两个名词:-)
祝好。

谢谢你的回复,现在明白了。盗用你的图例,这样理解应该就是正确的了。谢谢你的指点,正在拜读你的著作,以后免不了常来打搅
,晚安。
@3b阿当
呵呵,谢谢你的支持,有任何问题随时沟通,晚安啦:-)
您好,您对于kevinli 的解释我还没有弄懂,您说“但是Chicken方法表中同时存在其父类的方法,至于为什么调用了brid类的showI()方法,则是执行就近原则的解释”
但是您的例子里,bird 和 Chicken 也都有showType()方法,为什么不执行就近原则,因为是重写的原因?
谢谢
Anytao 老师你好:
我是新手,有几个问题想问一下
你看下面的代码
using System;
using System.Collections.Generic;
using System.Text;
namespace InheritSecret
{
public class Animal
{
public virtual void VirtualFun()
{
Console.WriteLine("i'm animal");
}
public void NonVirtualFun()
{
Console.WriteLine("I'm in Animal Class");
}
public void InvokeFun()
{
VirtualFun();
NonVirtualFun();
}
}
public class Dog : Animal
{
public override void VirtualFun()
{
Console.WriteLine("I'm dog");
}
public new void NonVirtualFun()
{
Console.WriteLine("I'm Dog Class");
}
}
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
dog.InvokeFun();
Console.ReadLine();
}
}
}
//The Result Is:
//I'm dog
//I'm in Animal Class
方法表里面有哪些函数?父类的非虚函数在不在里面?父类的构造函数在不在里面?
如果不在的话,有人说调用父类的方法表,那为什么父类的函数调用虚函数会调用子类的重载函数?父类方法表里面的虚函数应该没有被覆盖的啊
如果在里面的话,为什么父类的函数调用隐藏函数会调用父类的被隐藏函数?按道理说应该调用子类的隐藏函数啊
书中在解释关注对象原则的时候没强调 一个是覆写的虚方法不用关注引用类型啊。
那么到底是什么时候执行就近原则,什么时候关注对象原则呢。
希望AnyTao老师能给一个综合的例子,在解释一下。
谢谢
--引用--------------------------------------------------
仁面寿星: 您好,您对于kevinli 的解释我还没有弄懂,您说“但是Chicken方法表中同时存在其父类的方法,至于为什么调用了brid类的showI()方法,则是执行就近原则的解释”
但是您的例子里,bird 和 Chicken 也都有showType()方法,为什么不执行就近原则,因为是重写的原因?
谢谢
--------------------------------------------------------
我测试了一下好像和重写有关系呢
using System;
class A
{
public virtual void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}
class C : A
{
public override void Foo()
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
上面代码输出:Call on C.Foo()
如果这样的话
using System;
class A
{
public void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}class C : A
{
public void Foo() //这里应该就加个new
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
这里输出Call on A.Foo()
所以对于方法在用那两个原则的时候好像是要有前提条件的。
而原例中的bird.type输出Bird 是不是和
public string type = "Chicken";
有关,这里默认编译时候应该会加个new进行隐藏。隐藏了继承的成员“Bird.type”。
@仁面寿星
了解执行就近和关注对象,最关键的就是关注实际的内存分配情况,这一支配这两个原则的核心思想,对于你所提到的
--------------------------
但是您的例子里,bird 和 Chicken 也都有showType()方法,为什么不执行就近原则,因为是重写的原因?
谢谢
----------------------------------
我认为完全符合这两个原则的统筹,你可以从评论或者实例中详细分析,或者直接给我一个示例,我们针对示例进行进一步的讨论:-)
using System;
class A
{
public void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}class C : A
{
public void Foo() //这里应该就加个new
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
这里输出Call on A.Foo()
为什么这样就不执行关注对象原则呢。
到底和重写有没有关系呢。
或者anytao老师把这两个原则的使用具体情况讲一下呢
--引用-------------------------------------------------- Anytao: @仁面寿星了解执行就近和关注对象,最关键的就是关注实际的内存分配情况,这一支配这两个原则的核心思想,对于你所提到的 -------------------------- 但是您的例子里,bird 和 Chicken 也都有showType()方法,为什么不执行就近原则,因为是重写的原因? 谢谢 ---------------------------------- 我认为完全符合这两个原则的统筹,你可以从评论或者实例中详细分析,或者直接给我一个示例,我们针对示例进行进一步的讨论:-) --------------------------------------------------------
Anytao老师,这里您一直没回答一下当有重写的时候 对原则使用的影响呢。
using System;
class A
{
public virtual void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}
class C : A
{
public override void Foo()
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
上面代码输出:Call on C.Foo()
如果这样的话
using System;
class A
{
public void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}class C : A
{
public void Foo() //这里应该就加个new
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
这个例子能具体解释一下嘛。
今天一一来回答所有关于继承的问题,让大家等待的太久了
:-)
@仁面寿星
@HelloWorld1
@AGPSky
@AgpSky
看了AgpSky这么多的留言和示例之后,我发现了你的迷惑,和我之前没有特别声明的问题。
对于覆写来说其实是针对虚方法而言的,使得虚方法的调用决议于运行时的类型检查,从而可能表现出不同的调用结果;对于非虚方法而言,其在编译时皆可确定关联,因此,对于执行就近和关注对象来说这两个原则而言,其作用对象关注的是对虚方法的在运行时的调用分析,而不是非虚方法。
如本文分析的一样,.NET的每个类型都对应一个方法表,其中包括了继承的虚方法,自定义虚方法,自定义实例方法和静态方法等。并且指向被称为Method Stubs的结构,其中包括了类似call 00001234这样用于指向JIT Compiler,详细参见下图所示。方法第一次被调用时,CLR根据其声明时类型选择相应的call xxxxxxxx调用JIT编辑,并生成CPU指令保持在动态内存yyyyyyyy,同时更新Method Stubs中的call xxxxxxxx为jmp yyyyyyyy,用于在下次调用是直接寻址。这就是JIT编译的一般过程,可以相应的解释:
using System;
class A
{
public void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
}class C : A
{
public void Foo() //这里应该就加个new
{
Console.WriteLine("Call on C.Foo()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();
Console.ReadLine();
}
}
的执行结果,可见非虚方法是在编译时就根据其声明类型确定了执行过程。大多数情况下,我建议将公有方法声明为虚方法,这样可以避免父类于子类的名称冲突问题,事实上,Java语言的所有方法都被默认为虚方法。
对于非虚方法的调用,其决议于运行时,对客户来说其执行过程是“动态的”,子类可以通过override来覆写父类方法,因此必须清楚类型在内存中的详细情况,才能很好的分析方法的执行结果。这两个原则正是面向这一情况提出的判断方式,目的是为了在内存分配细节的基础上,给出一个更好理解的标准。所以,不管是关注对象,还是执行就近,都最终着眼于实际的内存分配状况。这是万变不离其综的原则。所以,我们可以继续分析AGPSky提出的另一个示例,做以简单的修改:
using System;
class A
{
public virtual void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
public virtual void M()
{
Console.WriteLine("Call on A.M()");
}
}
class C : A
{
public override void Foo()
{
Console.WriteLine("Call on C.Foo()");
}
public virtual new void M()
{
Console.WriteLine("Call on C.M()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();//关注对象
c1.M();//执行就近
Console.ReadLine();
}
}
上面代码输出:
Call on C.Foo()
Call on A.M()
首先应该明确的是,内存的情况,我以下面的例图做以简要的分析:
然后再根据两个原则来分析执行结果和过程:
由内存情况很容易可知,关注对象原则:c1指向了C实例,在方法表中继承A的两个虚方法,一个为Foo(),一个为M(),其中Foo被子类覆写,而M()被new阻断没有覆写,因此c1.Foo()调用的实际是子类覆写之后的方法C.Foo(),如图所示;而根据执行就近原则,c1本身被声明为A类型,因此调用c1.M()方法将执行A.M(),而不是C.M()。
所以,综上而言,两个原则是互为补充相辅相成的,我们同时要关注对象,同时要执行就近,分析的关键还是看内存的情况,方法表的指派。
写得很好,请教一个问题
public abstract class A
{
public A()
{
Console.WriteLine('A');
}
public virtual void Fun()
{
Console.WriteLine("A.Fun()");
}
}
public class B : A
{
public B()
{
Console.WriteLine('B');
}
public new void Fun()
{
Console.WriteLine("B.Fun()");
}
public static void Main()
{
A a = new B();
a.Fun();
}
}
为什么输出
A
B
A.Fun()而不是
A
B
B.Fun()
AnyTao老师在请教一个问题
关于继承接口的
下面是我看到的一个例子
接口是这样定义的
public interface IDbBase
{
DbCommand CreateCommand();
DbConnection CreateConnection();
DbDataAdapter CreateDataAdapter();
DbParameter CreateParameter();
}
下面是是继承接口的类
public class DbBase : IDbBase
{
DbCommand IDbBase.CreateCommand()
{
return new SqlCommand();
}
DbConnection IDbBase.CreateConnection()
{
return new SqlConnection();
}
DbDataAdapter IDbBase.CreateDataAdapter()
{
return new SqlDataAdapter();
}
DbParameter IDbBase.CreateParameter()
{
return new SqlParameter();
}
protected string Pre;
public DbBase()
{
Pre = DBConfig.TableNamePrefix;
DbHelper.Provider = this;
}
}
为什么这个子类的方法要加上接口名呢。
其他的接口继承不用加啊。这个怎么回事呢。
@AGPSky
接口实现,一般可分为隐式接口实现和显式接口实现,你这里示例的正式显式接口实现的例子。
而这两种实现方式在使用上、应用上和实现机制上都有所不同,例如:显式接口实现以接口实例来访问,而隐式接口实现以类实例来访问等;二者的内部实现机制等。
之所以存在显式接口实现,一定程度上是避免接口成员之间的同名混淆:
interface ITalk
{
void Talk();
}
public class Base : ITalk
{
public void Talk()
{
Console.WriteLine("基类方法");
}
void ITalk.Talk()
{
Console.WriteLine("接口方法");
}
}
@chunfeng
呵呵,评论中已经有很多这个问题了,谢谢你的参与;-)
@Anytao
隐式接口也可以实现以接口实例来访问吧?
发现实现的时候方法的修饰符有区别,为什么显示的时候修饰符可以没有。而隐式实现需要呢。
@zzz
是阿,静态成员当然能够继承,而且还是以类来访问。
@AGPSky
隐式接口实现以接口实例访问,必须进行类型转换才能进行相应的访问,这和直接访问是有区别的。
关于修饰符问题,我的理解是默认修饰符的级别不同而造成的,类成员是默认为private的,而接口成员是默认为public的,所以显式修饰符本身已经有了public的标记,而隐式接口由于访问级别问题,必须要求实现为public才能继承层次的访问级别。
博主,真的很高兴能看到你这么多精彩的分析文章。
我想问个小问题:
关于 Interface 与 Object 。
为何定义了 一个Interface变量,却可以在此变量上调用这些 GetType(),GetHashCode(),ToString()等的属于 Object 类的方法呢?难道 Interface也继承自Object了吗?可看IL代码,我没有发现它们之间有这种继承的关系。
恳请博主帮我解答一下,不胜感激!
@从天而降
一个很有趣的问题。
对于接口而言在本质上你可以认为接口就是一个class,而任何实现了Interface的实例,都不可避免的同时继承了Object,所以任何接口实例都可以直接调用Object内定义的public或protect方法。
这一点,你可以从IL反编译代码中看到:
.class public auto ansi beforefieldinit Test
extends [mscorlib]System.Object
implements ITest
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
}
}
我是初学者,我知道程序的运行结果,我的思路很简单,只要明白多态、虚函数以及new的作用后,很容易推出结果。我试着看懂作者的意思,顺着作者您的思路考虑问题,却屡屡受挫!这是为什么呢?
作者您解释调用了谁的方法是不是站在对象的立场来解释的?
比方说这里:
Bird bird2 = new Chicken();//您是不是站在Chicken的角度来解释的呢?
我是依据birdi2来判断调用谁的方法的!这一点我肯定。
问一下,单单只执行Chicken chicken = new Chicken();
那方法表中是不是有两个表,一个是chicken的方法表,一个是其父类的方法表
@tao1234567899999
受挫的原因是什么呢?从大部分同志的反应来看,从本质的角度来理解继承和多态是最受用的办法,也无所谓站的角度。如果还有疑问可以再读原文,或者将您的疑惑留言,我们继续讨论:-)
@SAL
非也,一个类型将只对应一个方法表,其TypeHander将指向本类型的方法表内存,同一类型的不同实例则共用相同的方法表。
楼主你好:
最近在拜读大作,有点疑问望指教:如果在Animal中也加入了type字段,赋值为"animal",按照就近原则,bird2.type的值仍然是"Bird",但既然bird也继承于Animal,bird2.type的值为什么不是"animal",望解惑,先谢谢了!
对象一经创建,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止
---------------------------------------------------------------
您好,问个问题,比如某个页面执行的时候,会创建很多类,而这些类的根对象都是System.Object ,当页面执行时候第一个对象创建的时候顺便在内存中创建了一个 System.Object 根对象,当页面第二个对象创建时候是继续用前面已经存在的System.Object 还是重新又创建个System.Object 对象?谢谢
1. 上面我们分析到bird2.type的值是“Bird”,那么bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据本文上面的分析,想想到底为什么?
这个是为什么呢?就近原则和关注对象原则看起来有点矛盾呀
如果你看了这篇文章,下面的例子结果你能正确的说出来吗
我想还有人是迷糊的。
class A
{
public A()
{
PrintFields();
}
public virtual void PrintFields(){}
}
class B:A
{
int x=1;
int y;
public B()
{
y=-1;
}
public override void PrintFields()
{
Console.WriteLine("x={0},y={1}",x,y);
}
}
public class test
{
public static void Main()
{
B b=new B();
//A a=new A();
//a.PrintFields();
b.PrintFields();
}
}
您好,请问下, Bird animal = new Bird();
animal,此时占内在不??如果占用位置在哪???
我是这样理解的,new Bird(),在托管堆创建一个实在对象,占有内存A, Bird animal,声明一个引用变量,相当在stack中开了一块空间,空间的代替名,里面放的值是A的地址。不是不这样的???打扰
showType(),还好理解,在于这是子类覆盖了父类方法,
如果bird 和chiken都new 了一个Eat(),
那么bird = chiken;
bird 指向new Chiken(),那么new Chiken()时,是不是创建完字段后,创建方法,生成的eat方法顺序应该是:
animial.eat,
bird .eat
chiken.eat
那按你说的那不是bird = chiken
bird.eat
输出是animal.eat
是的,只按就近原则,这样是解析不通的
很显然类型也在起作用
比如:Bird bird2 = new Chicken();
应该是由于这个Bird类型导致优先取Bird_type
如果只按就近原则那下面这样不也是Bird_type了吗
Chicken bird2 = new Chicken();
Anytao大哥:
你好!最近正在读你的书,觉得受益匪浅,先代表众多.NET学习者感谢你的辛勤劳动。我有几个问题,请你赐教。
(1)在Animal、Bird、Chicken的方法表中是不是都只保存了一个ShowType()方法。也就是说Bird类的ShowType()方法也是一个Virtual函数,在Chicken类中仍会将其覆写,是不是只有当某个子类中用new关键字实现了ShowType()方法后才会增加一个新的ShowType()方法;
(2)既然子类中没有父类非虚方法的拷贝,那么子类是怎样实现对父类非虚方法的调用的呢?
(3)"引用类型不同的区别决定了不同的对象在方法表中不同的访问权限" ,那么引用类型不同会不会决定字段的访问权限呢?
我把Bird和Chicken类中type改成public,然后输出它们的type:
static void Main(string[] args)
{
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();
Console.WriteLine(bird2.type);
Console.WriteLine(chicken.type);
Console.ReadLine();
}
输出是:bird chicken ,如果是访问权限的问题,那是怎么实现的呢?
(4)对动态绑定机制我还是不太理解,哪些是静态绑定,哪些是动态绑定,能不能简要地解释一下。
请王大哥赐教,谢谢!
@Anytao
如果要强调base调用父类方法是去哪儿找其父类方法这个问题,可以这样来描述,首先找到父类对象,然后根据父类对象的方法表指针定位到该方法,请注意:上述关于子类覆写发生在子类而非父类,父类方法表中仍然是父类本身的方法,你可以从本文的那张模拟图中找到答案。
lz打扰了,抱着这个问题我终于在这里找到比较好的文章。^-^
比如:创建这样的对象 Bird animal = new Bird();
那在堆里分配一块内存给Bird 时,需要取到父类的字段或者方法地址,那是不是一定也会在内存里创建它父类对象?也就是说创建子类时会不会创建父类对象?那java那边是不是也是一样采取这样的方法?谢谢啦
根据我们上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针
请问这里所指的引用指针类型在哪里体现,是在栈上还是堆上,谢谢
写的真好。如果在父类和子类加入一些不同名的方法就更清楚了:)
借用74楼的代码:
using System;
class A
{
public virtual void Foo()
{
Console.WriteLine("Call on A.Foo()");
}
public virtual void M()
{
Console.WriteLine("Call on A.M()");
}
}
class C : A
{
public override void Foo()
{
Console.WriteLine("Call on C.Foo()");
}
public virtual new void M()
{
Console.WriteLine("Call on C.M()");
}
}
class D
{
static void Main()
{
A c1 = new C();
c1.Foo();//关注对象
c1.M();//执行就近
Console.ReadLine();
}
}
上面代码输出:
Call on C.Foo()
Call on A.M()
这很好理解。但是对于C c2 = new C();c2.M()的执行应该可以用下面的文字进行解释:
非虚方法和虚方法调用不同,当JIT编译以后,非虚方法调用是先将当前对象压栈,然后直接call方法地址,而不像虚方法调用的时候有查表的过程。