[你必须知道的.NET]第十五回:继承本质论

[你必须知道的.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关键字,则意味着隐藏基类实现,其实就是创建了与父类同名的另一个方法,在编译中这两个方法处于动态方法表的不同地址位置,父类方法排在前面,子类方法排在后面。

 

4. 结论

在.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 阅读(5842) 评论(92)  编辑 收藏 所属分类: 01 [你必须知道的.NET]

  回复  引用    
#1楼 2007-09-11 00:40 | kisskiki [未注册用户]
作者的文风有了改变了。文章看的是意犹未尽啊
  回复  引用  查看    
#2楼 [楼主]2007-09-11 08:37 | Anytao      
@kisskiki
呵呵,被你看出来了,其实还有写内容想写进去,例如继承和聚合的比较、继承的局限、继承和多态等等,还是下回吧:-)
  回复  引用  查看    
#3楼 2007-09-11 09:34 | 麒麟.NET      
十分好文
  回复  引用  查看    
#4楼 2007-09-11 09:40 | thh      
父类不能调用子类方法,这个有错误吧。
虚函数在子类可以被重写,然后被父类调用吧。
这些东西没有太大的意义,只能算基本的。
  回复  引用  查看    
#5楼 2007-09-11 09:44 | Anders Cui      
感觉少了构造函数那一部分的分析 :)
  回复  引用    
#6楼 2007-09-11 10:39 | heaven [未注册用户]
受益了
谢谢
我喜欢从编译器和IL的角度看问题

  回复  引用  查看    
#7楼 2007-09-11 10:54 | today      
继承是面向对象的特征,本人认为继承的精髓在于它可以解决什么问题,而不是楼主介绍的在.net里的实现机制。因为实现机制都是变化的,因公司,因人的喜好不同,实现机制也有不同(比如c#和java的不同)。感觉有点吹毛求疵,南辕北辙了。特别说明,我仅仅是发表我对这篇文章的看法,没有别的意思,不要误会。
  回复  引用  查看    
#8楼 [楼主]2007-09-11 11:20 | Anytao      
@麒麟.NET
:-)
  回复  引用  查看    
#9楼 [楼主]2007-09-11 11:25 | Anytao      
@thh
父类当然不可调用子类方法,因为父类对象中根本没有子类方法,关于虚函数动态调用正确的说法是:父类指针可以调用子类对象。在动态调用时,父类还是调用其本身的方法,只不过这个方法被子类方法覆写罢了,但是在概念上和“父类可以调用子类方法”完全是两回事。
关于基础只说,只能是仁者见仁了,:-)。
  回复  引用  查看    
#10楼 [楼主]2007-09-11 11:26 | Anytao      
@Anders Cui
呵呵,的确如此,关于构造函数的创建很有必要加以论述,可能在系列的后续中有所涉猎,敬请关注。
  回复  引用  查看    
#11楼 [楼主]2007-09-11 11:26 | Anytao      
@heaven
很有同感,感谢关注。
  回复  引用  查看    
#12楼 [楼主]2007-09-11 11:30 | Anytao      
@today
感谢你的意见。
我认为了解其运行机制和了解其应用方向是两个必不可少的方面,缺一不可。从内存的布局、分配,我们就可以很清楚也很深刻的了解动态调用的实现原理,从而对继承、对多态,包括面向对象都有更好的了解。
关于Java和.NET的实现机制,至少在这一块也都大同小异,有区别但不大。
我始终认为从本质入手来了解技术,是为应用技术做了最好的铺垫。
一家之言,仅供参考。谢谢。
  回复  引用    
#13楼 2007-09-11 12:07 | 双眼皮 [未注册用户]
Anytao说:关于Java和.NET的实现机制,其实也都大同小异,有区别但不大。java默认所有的父类方法是虚的,而C#不是虚的,这一点的区别还是挺大的。today说:实现机制都是变化的,java或c#继承的实现机制还会有可能变化吗?你说因公司,因人的喜好不同,实现机制也有不同(比如c#和java的不同),这话精典,我是设计者的话,我想怎样规定就怎样规定了:),讨论这个话题应该不是吹毛求疵,南辕北辙吧,你怎么理解Bird bird = new Chicken()?你怎么知道bird.Method()执行的是Bird中的方法还是Chicken中的方法?背下来的?还是另有其他方法来理解这个事情?请today兄不吝指教一下,我认为从编译的角度看对象,是最好的。
  回复  引用  查看    
#14楼 2007-09-11 13:25 | today      
@双眼皮
其实,按照Anytao 兄的方法,是学习继承比较好的方法。但这太局限于某种语言了,不同的人发明的面向对象的语言,都会支持继承,但又都会有差异,所以,语言仅仅是工具,而继承隐藏的思想(解决问题的能力)却值得寻味。关于你提到的另外一个问题,我是如何知道的,其实这里的关键不在于怎么知道答案的,而是因为答案就是这样子的,.net的设计者就是这样设计的。如果有需求的话,这里的结果完全可以是调用父类的方法(当然父类必须存在这样的方法)。沿着这种思路,很有可能会出现“反继承”。
当然,我的意思并不是说这篇文章写得不好,因为,我也相信探微才可知著。

  回复  引用  查看    
#15楼 2007-09-11 14:27 | Gary Gong      
有点漂,感觉像在飞。
  回复  引用  查看    
#16楼 [楼主]2007-09-11 20:25 | Anytao      
@today
反继承的说法,倒是很有新意。还是那句话,各取所需就行了,对继承的讨论本文也远不是终点,所以today兄提到的关于如何继承,怎么继承的这些问题,留待后文吧。
  回复  引用  查看    
#17楼 [楼主]2007-09-11 20:26 | Anytao      
@Gary Gong
:-)
  回复  引用  查看    
#18楼 2007-09-12 11:15 | 紫色阴影      
Anytao兄:
我感觉你这个图画得有点问题,子类的方法表中好像不包括父类的非虚方法
你可以看看这篇文章:http://www.cnblogs.com/blusehuang/archive/2007/07/27/833593.html
  回复  引用  查看    
#19楼 [楼主]2007-09-12 13:24 | Anytao      
@紫色阴影

按照我的理解,子类中继承父类方法正是通过拷贝父类方法到其方法表中,不管是虚方法还是非虚方法,否则子类对象又如何获取父类方法呢?
关于你的那篇文章中的图是怎么生成的,很想知道这一点。看来还值得探讨一下。

当然,这是我的理解,希望有机会讨论:-)
  回复  引用  查看    
#20楼 2007-09-12 14:33 | 紫色阴影      
@Anytao
我是用windbg跟踪查看的,图也是在此基础上画的

子类的方法表里面的确不包含父类非虚方法定义
非虚方法和虚方法调用不同,当JIT编译以后,非虚方法调用是先将当前对象压栈,然后直接call方法地址,而不像虚方法调用的时候有查表的过程。

非虚方法可以被内联,如果非虚方法调用也要查找方法表,那是做不到内联的
  回复  引用  查看    
#21楼 [楼主]2007-09-12 14:41 | Anytao      
@紫色阴影
的确值得研究一下,也想到是用WinDB来跟踪看的,我再了解一下,还不是很清楚,这是我期望的收获,受教:-)
  回复  引用  查看    
#22楼 2007-09-12 18:59 | 孙会生      
anytao兄的分析很入理,我一直以来都是拿来一个东西就用,全不管为什么要 这么用.看了你的系列文章,真的受益啊.
对于底层的东西了解的多了,东西才能用的更有技巧.

  回复  引用  查看    
#23楼 [楼主]2007-09-12 19:30 | Anytao      
@孙会生
谢谢,希望对你有所帮助,继续关注会更多。共同进步。
  回复  引用    
#24楼 2007-09-13 23:32 | 1+1 ?= 2 [未注册用户]
1+1为什么=2啊?
人从哪里来?死了到哪里去啊??
LZ为什么不去研究呢?
继承.NET规定死了就是这种机制了。你还非得去研究。还号召大家一起来研究。
你研究透了继承的本质能对你提高继承的效率有帮助?对你的设计思想有帮助?这太牵强了吧。9×9乘法表为什么要那么乘呢??
那照你这么说,为什么不直接去研究0101机器码不是更加底层更加有用??
  回复  引用    
#25楼 2007-09-13 23:36 | 1+1 ?= 2 [未注册用户]
不见得知道了编译器的指针调用后,他的抽象能力就能有多高。
面向对象的世界还去研究底层汇编,干脆做C做汇编不是更好?
面向对象讲的是对对象,对事务,甚至对行业的抽象。
  回复  引用  查看    
#26楼 [楼主]2007-09-13 23:53 | Anytao      
@1+1 ?= 2

所谓志不同,道有异。
人从那里来,又到哪里去,肯定有人专门研究,你不能说他们是疯子。

.NET规定死的东西都不去研究,请问哪些又是活的,有些时候从0和1的角度去看问题并非坏事,从计算机本身的角度来理解计算机才叫体察入微,这和从继承机制角度来理解继承是一回事儿,如果非要问个问什么,那就问自己志在哪里,道在何方?每个人的情况是不一样的,如何只想沉迷于拖拽控件的程度上,谁也拦不住你对个人理想的冲动。

另外,谈到对设计思想的帮助,我自己的体会是有过之而无不及,可能更多的感想你未必理解,单纯以对象、事务、抽象来粉饰面向对象,只能停留在面向对象的光环下放光而已。不知1+1 ?= 2兄是否细读本文,如果是,那就再读一次,看看对继承的理解是否有益;如果不是,那也没有讨论的必要,谢谢。

希望常来,切磋一二。
  回复  引用  查看    
#27楼 2007-09-15 18:28 | 海天一鸥      
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)。

  回复  引用    
#28楼 2007-09-15 22:01 | tongxl [未注册用户]
文中“然后,是方法列表的创建,必须明确的一点是对象的创建是在运行时,而方法列表的创建是在编译时”有误。

方法表也是在运行时创建的,编译时创建的怎么会在托管堆(内存)中呢! 编译时生成的是元数据和IL代码。

在运行时CLR通过查找元数据建立方法表(其实是对象的类型所对应的数据结构), 然后把每个方法条目指向CLR的一个函数(暂叫JitCompiler),当一个方法执行时,比如A(),由于A方法条目指向JitCompiler,所以JitCompiler被调用,JitCompiler函数通过元数据找到A方法的IL代码然后把它翻译成本地CPU指令,这些指令保存在一个动态内存中,然后把A方法条目指向该内存地址,并跳到该内存地址上执行,当A方法第二次被调用时,由于A方法条目已经指向本地CPU指令所在内存地址(而不是JitCompiler函数的内存地址),所以直接执行之。这就是常所的只在第一次执行时才把IL代码翻译成CPU指令。
  回复  引用  查看    
#29楼 [楼主]2007-09-18 22:11 | Anytao      
@海天一鸥
:-)
谢谢你的支持,博客是个接受自由的地方,讨论技术是需要这种自由,:-)
  回复  引用  查看    
#30楼 [楼主]2007-09-18 22:13 | Anytao      
@tongxl
的确有误,谢谢你的指正,我再研究研究然后做以合适的修订,非常感谢,希望你常来斧正,:-)
  回复  引用  查看    
#31楼 [楼主]2007-09-18 22:13 | Anytao      
@chinaifne
:-)
  回复  引用    
#32楼 2007-09-24 17:01 | tt1 [未注册用户]
真的很不错。受益了。
这篇文章看了很多次,
终于算是理解了。
  回复  引用  查看    
#33楼 [楼主]2007-09-26 08:43 | Anytao      
@tt1
文中有个别地方表达有误,可以参考一评论,近期将做以修订,谢谢。
  回复  引用  查看    
#34楼 [楼主]2007-10-04 11:46 | Anytao      
今天无意中看了idior早前的一个关于继承测试的示例:
http://idior.cnblogs.com/archive/2005/03/04/113143.html
我发现以继承的本质出发来回答这个问题就可以变得简单,再结合idior的另一篇
http://www.cnblogs.com/idior/archive/2004/12/13/76288.html大作来理解,会让你在继承的问题上豁然开朗。
  回复  引用  查看    
#35楼 2007-10-06 14:15 | lbq1221119      
太经典了
  回复  引用  查看    
#36楼 [楼主]2007-10-06 23:08 | Anytao      
@lbq1221119
呵呵,希望经常来讨论:-)
  回复  引用  查看    
#37楼 2007-10-17 14:02 | 小猴子      
LZ文章不错,受益不浅。。。
以前有想,不过LZ深入。
赞同LZ观点,只有了解了基础与实质,才能更好的创造好的程序。
  回复  引用  查看    
#38楼 [楼主]2007-10-17 19:23 | Anytao      
@小猴子
很有同感,研究.NET还是离不开多少知道点CLR运行机制,这也是这个系列和CLR团队所期望达到的目标:-)
  回复  引用    
#39楼 2007-10-19 17:39 | woanon [未注册用户]
bird2.ShowType()会显示什么值呢?答案是“Type is Chicken”,根据本文上面的分析,想想到底为什么?

本人愚笨,这个没看懂.....
  回复  引用  查看    
#40楼 [楼主]2007-10-19 18:00 | Anytao      
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关键字对父类方法的隐藏作用。

  回复  引用    
#41楼 2007-10-23 10:48 | 孤舟垂钓 [未注册用户]
lz您好,想跟你请教一个问题。您文中说“首先将Bird的所有方法拷贝一份,然后和Chicken本身的方法列表做以对比,如果有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。
”既然子类方法覆盖同名的父类方法,那么当base父类的方法时,请问上哪去找父类的方法?谢谢你的解惑。
  回复  引用  查看    
#42楼 [楼主]2007-10-23 16:16 | Anytao      
@孤舟垂钓
其实二者并不矛盾,首先关于子类虚方法覆写父类方法的机制问题,本文已经有了较详细的论述,所以暂不讨论。
我想你主要关心的是base关键字的使用问题。本人另外一篇文章《第六回:深入浅出关键字---base和this 》http://www.cnblogs.com/anytao/archive/2007/05/04/must_net_06.html对此有一定的论述,仅供参考。
简单来说,.NET提供了base关键字来主要完成两件事:一是在派生类中访问基类被覆写的方法;二是在派生类中调用基类的构造函数。这就像提供this关键字来当前实例一样,就是.NET提供的一种机制。
如果要强调base调用父类方法是去哪儿找其父类方法这个问题,可以这样来描述,首先找到父类对象,然后根据父类对象的方法表指针定位到该方法,请注意:上述关于子类覆写发生在子类而非父类,父类方法表中仍然是父类本身的方法,你可以从本文的那张模拟图中找到答案。

有些罗唆,见谅。
  回复  引用    
#43楼 2007-10-23 17:12 | 孤舟垂钓 [未注册用户]
@Anytao
非常感谢你的答疑。没想到我上午才发,下午你就很耐性的回复了,很是感动,感觉你是一个很有责任感、对技术很执着的人(申明一下,我不是在拍马屁),好像还是个性情中人,哈哈,不知猜得对不对。很想跟你交个朋友。请原谅我的固执,我还想继续问问题,不要拍砖哦。那个父类对象怎么找?我不是在抬扛,真想把问题弄清楚
  回复  引用  查看    
#44楼 [楼主]2007-10-23 20:48 | 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可以访问任意上级父类的方法。


  回复  引用    
#45楼 2007-10-24 08:56 | 孤舟垂钓 [未注册用户]
@Anytao
lz,8点还在笔耕,向你学习。惭愧啊,那会我在家做饭呢。谢谢,基本上清楚了
  回复  引用  查看    
#46楼 [楼主]2007-10-24 09:07 | Anytao      
@孤舟垂钓
呵呵,欢迎随时讨论,也谢谢你的关注,关于base的一些问题可以参见http://www.cnblogs.com/anytao/archive/2007/05/04/must_net_06.html
的论述,关于继承这块,把握住几个关键的地方就会清楚一些:
1 创建的是子类还是父类?指向的是子类还是父类?
2 是否有覆写或new中断?
3 构造函数的执行顺序?

《.NET 本质论》一书对此有详细的解释,向你推荐:-)
  回复  引用  查看    
#47楼 2007-11-08 15:21 | Boler Guo      
@Anytao
这篇文章的两个问题已经被紫色阴影和tongxl指出来了,别忘了修改呀,要不然会让大家困惑的。

问题一:子类方法表中没有父类的非虚方法
问题二:方法表肯定是运行时创建的,是在运行时当CLR需要JIT某个方法的时候,会把这个方法里面所有还没有加载到堆中的所有Types(的方法表)加载到堆中

  回复  引用  查看    
#48楼 [楼主]2007-11-08 18:32 | Anytao      
@Boler Guo
说得及时,忙起来把这个事儿就忘了,这两天尽快改了,谢谢你的提醒,要不然又是一种罪过。:-)
  回复  引用  查看    
#49楼 [楼主]2007-11-27 17:46 | Anytao      
@Boler Guo
已做修改,感谢紫色阴影、tongxl和Boler Guo
  回复  引用    
#50楼 2007-12-10 23:04 | qhDocument [未注册用户]
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();

这两个对象区别还是有些模糊,望Anytao指教!!!


  回复  引用  查看    
#51楼 [楼主]2007-12-11 00:16 | 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来保证。所以二者虽然指向了同一块地址,但是其可以访问的范围是有区别的。

不知这样解释,是否清楚:-)


  回复  引用  查看    
#52楼 [楼主]2007-12-11 00:18 | Anytao      
@qhDocument

打个不是很恰当的比方:假如有父子俩在沙漠里向一片绿洲看去,它们俩看的是同一块地方。但是父亲个子高,会看得多一点,而儿子个子矮,只能看得少一点。而这种情况在继承中刚好相反,子类的访问权限较父类大,所以chicken可以调ShowColor,而bird2不可以调用。子类可以代替父类,但是父类不能代替子类。
  回复  引用    
#53楼 2008-03-01 22:37 | zzz [未注册用户]
静态成员居然也能继承,真是奇怪
  回复  引用  查看    
#54楼 2008-05-29 14:18 | Ivan-Yan      
@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,输出即可
  回复  引用  查看    
#55楼 2008-05-29 14:21 | Ivan-Yan      
@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);

}

}

}

  回复  引用  查看    
#56楼 [楼主]2008-06-01 22:47 | Anytao      
@Ivan-Yan
首先谢谢你的讨论,最近很忙没能及时回复,很抱歉:-)

关于Bird b = new Chicken();的讨论很正确。

正如你的测试代码看到的那样,这种方法是经得住考验的,因为从内存角度来看问题,正是最可靠的方式:-)

继承是个重要的话题,从内存角度来理解继承,除了能够清晰的看清一切障眼的法门,能重要的是可以提高对于OO的感悟。
  回复  引用    
#57楼 2008-06-11 11:09 | kevinli [未注册用户]
你好: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()方法呢?



  回复