深入浅出话多态(上)——具体而微

小序

前几天写了一篇《深入浅出话委托》,很多兄弟姐妹发Mail说还算凑合,又有兄弟说能不能写一篇类似的文章,讲解一下什么是“多态”。一般情况下我写文章都是出于有感而发:一来做个思考的总结(怕时间长了就忘记了),二来与大家分享一下。“多态”实在是个大概念,我没有仔细研究过,更不消说在实践中有深入的使用,所以本文纯属硬着头皮上——如果内容有什么闪失,请大家别客气——猛拍砖就是了。

上面一段是前几天写的!昨天晚上看了巴西进八强的比赛,我虽然是个绝对的伪球迷,但我也能看出来人家肥罗的球技啊!人家的意识,丝毫不像是在踢世界杯,纯粹就是表演……台上三分钟,台下十年功啊!我们一起练程序,就要把代码写到这个程度,让我们一起无限量提高自己的技术吧!

正文

一.什么是多态(Polymorphism)

多态(Polymorphism)是面向对象(Object-Oriented,OO)思想“三大特征”之一,其余两个分别是封装(Encapsulation)和继承(Inheritance)——可见多态的重要性。或者说,不懂得什么是多态就不能说懂得面向对象。

多态是一种机制、一种能力,而非某个关键字。它在类的继承中得以实现,在类的方法调用中得以体现。

先让我们看看MSDN里给出的定义:

Through inheritance, a class can be used as more than one type; it can be used as its own type, any base types, or any interface type if it implements interfaces. This is called polymorphism. In C#, every type is polymorphic. Types can be used as their own type or as a Object instance, because any type automatically treats Object as a base type.

译文:通过继承,一个类可以被当作不止一个数据类型(type)使用,它可以被用做自身代表的数据类型(这是最常用的),还可以被当作它的任意基类所代表的数据类型,乃至任意接口类型——前提是这个类实现了这个接口。这一机制称为“多态”。在C#中,所有的数据类型都是多态的。任意一个数据类型都可以被当作自身来使用,也可以当作Object类型来使用(我怀疑原文有问题,那个instance可能是原作者的笔误),因为任何数据类型都自动以Object为自己的基类。

呵呵,除非你已经早就知道了什么是多态然后翻过头来看上面一段话,不然我敢打保票——我是清清楚楚的,你是稀里糊涂的。OK,不难为大家了,我用几个句子说明一下多态的思想。

我们先把前文中提到的“接口”理解为“一组功能的集合”,把“类”理解为功能的实现体。这样的例子多了去了。我们就拿生物界做比喻了:

功能集合1:呼吸系统

功能集合2:血液循环系统

功能集合3:神经系统

功能集合4:语言系统

类1:灵长类动物。此类实现了1到3功能集合。

类2:猴子类。继承自类1。新添加了“爬树”的功能。

类3:人类。继承自类1。同时实现了功能集合4。

类4:男人类。继承自类3。新添加了“写程序”的功能。

类5:女人类。继承自类3。新添加了“发脾气”的功能。

作业:请大家把上面的关系用图画出来

OK,让我们看下面的话,判断对错:

1.        男人是男人             (√)            原因:本来就是!

2.        男人是人                (√)            原因:人类是男人类的基类

3.        男人是灵长类动物    (√)            原因:灵长类是男人类的更抽象层基类

4.        男人是会说话的       (√)            原因:男人类的基类实现了语言系统

5.        女人是猴子             (×)            原因:如果我这么说,会被蹁死

6.        猴子是女人             (×)            原因:女人不是猴子的基类

7.        人会写程序             (×)            原因:写程序方法是在男人类中才具体实现的

8.        女人会发脾气          (√)            原因:因为我说5……

哈哈!现在你明白什么是多态了吧!其实是非常简单的逻辑思维。上面仅仅是多态的一个概念,下面我们通过代码去研习一下程序中的多态到底是什么。

二.多态的基础——虚函数(virtual)和重写(override)

很多公司在面试的时候常拿下面几个问题当开胃小菜:

1.        如何使用virtual和override?

2.        如何使用abstract和override?

3.        “重写”与“重载”一样吗?

4.        “重写”、“覆盖”、“隐藏”是同一个概念吗?

顺便说一句:如果你确定能把上面的概念很熟练的掌握,发个Mail给我(bladey@tom.com ),也许你能收到一份薪水和福利都不错的Offer :p

今天我们学习多态,其实就是解决问题1。前面已经提到过,多态机制是依靠继承机制实现的。那么,在常规继承机制的基础之上,在基类中使用virtual函数,并在其派生类中对virtual函数进行override,那么多态机制就自然而然地产生了。

小议virtual

呵呵,我这人比较笨——有我的老师和同学为证——学东西奇慢无比,所以当初在C++中学习virtual的历程是我心中永远挥之不去的阴影……倒霉就倒霉在这个“虚”字上了。“实”的我还云里雾里呢,更何况这“虚”的,“虚”的还没搞清楚呢,“纯虚”又蹦出来了,我#@$%!^#&&!……

还好,我挺过来了……回顾这段学习历程,我发现万恶之源就是这个“虚”字。

在汉语中,“虚”就是“无”,“无”就是“没有”,没有的事情就“不可说”、“不可讲”——那还讲个X??老师也头疼,学生更头疼。拜初中语文老师所赐,我的语言逻辑还算过关,总感觉virtual function译为“虚函数”有点词不达意。

找来词典一查,virtual有这样一个词条:

Existing or resulting in essence or effect though not in actual fact, form, or name:

实质上的,实际上的:虽然没有实际的事实、形式或名义,但在实际上或效果上存在或产生的:

例句:

the virtual extinction of the buffalo.

野牛实际上已经绝迹(隐含的意思是“尽管野牛还木有死光光,但从效果上来讲……”)

啊哦~~让我想起一句话:

有的人活着他已经死了; 有的人死了他还活着……

不禁有点惊叹于母语的博大精深——

virtual function中的virtual应该译做“名存实亡”而不是“虚”!

OK,下面就让我们看看类中的virtual函数是怎么个“名存实亡”法。

例子1 virtual / override程序

//                          水之真谛                                     //
//            http://blog.csdn.net/FantasiaX               //
//                   上善若水,润物无声                             //

using System;
using System.Collections.Generic;
using System.Text;

namespace Sample
{
       // 演员(类)
       class Actor
       {
              public void DoShow()
              {
                     Console.WriteLine("Doing a show...");
              }
       }

// 乐手(类),继承自Actor类
       class Bandsman : Actor
       {
              // 子类同名方法隐藏父类方法
              // 其实标准写法应该是:
              // public new void DoShow(){...}
              // 为了突出"同名",我把new省了,编译器会自动识别
              public void DoShow()
              {
                     Console.WriteLine("Playing musical instrument...");
              }
       }

// 吉他手(类),继承自Bandsman类
       class Guitarist : Bandsman
       {
              public new void DoShow()
              {
                     Console.WriteLine("Playing a guitar solo...");
              }
       }

class Program
       {
              static void Main(string[] args)
              {
                     // 正常声明
                     Actor actor = new Actor();
                     Bandsman bandsman = new Bandsman();
                     Guitarist guitarist = new Guitarist();

// 一般情况下,随着类的承继和方法的重写
                     // 方法是越来越具体、越来越个性化
                     actor.DoShow();
                     bandsman.DoShow();
                     guitarist.DoShow();

Console.WriteLine("===========================");
                    
                     //尝试多态用法
                     Actor myActor1 = new Bandsman();               //正确:乐手是演员
                     Actor myActor2 = new Guitarist();                  //正确:吉他手是演员
                     Bandsman myBandsman = new Guitarist();   //正确:吉他手是乐手

//仍然调用的是引用类型自身的方法,而非派生类的方法
                     myActor1.DoShow();
                     myActor2.DoShow();
                     myBandsman.DoShow();
              }
       }
}

代码分析:

1.        一上来,演员类、乐手类、吉他手类形成一个继承链。

2.        乐手类和吉他手类作为子类,都把其父类的DoShow()方法“隐藏”了。

3.        特别强调:“隐藏”不是“覆盖”,后面要讲的“重写”才是真正的“覆盖”。

4.        隐藏是使用new修饰符实现的,但这个修饰符可以省略。

5.        隐藏(Hide)的含意是:父类的这个函数实际上还在,只是被子类的同名“藏起来”了。

6.        重写(override)与覆盖是同一个含意,只是覆盖并非编程的术语,但“覆盖”比较形象。

7.        主程序代码的上半部分是常规使用方法,没什么好说的。

8.        主程序代码的下半部分已经算是多态了,但由于没有使用virtual和override,多态最有价值的效果——个性化方法实现——没有体现出来。后面的例子专门体现这一点。

例子2 应用virtual / override,真正的多态

//                          水之真谛                                     //
//            http://blog.csdn.net/FantasiaX               //
//                   上善若水,润物无声                             //

using System;
using System.Collections.Generic;
using System.Text;

namespace Sample
{
       // 演员(类)
       class Actor
       {
              // 使用了virtual来修饰函数
              // 此函数已经"名存实亡"了
              public virtual void DoShow()
              {
                     Console.WriteLine("Doing a show...");
              }
       }

// 乐手(类),继承自Actor类
       class Bandsman : Actor
       {
              // 使用了override来修饰函数
              // 此函数将取代(重写)父类中的同名函数
              public override void DoShow()
              {
                     Console.WriteLine("Playing musical instrument...");
              }
       }

// 吉他手(类),继承自Bandsman类
       class Guitarist : Bandsman
       {
              public override void DoShow()
              {
                     Console.WriteLine("Playing a guitar solo...");
              }
       }

class Program
       {
              static void Main(string[] args)
              {
                     // 正常声明
                     Actor actor = new Actor();
                     Bandsman bandsman = new Bandsman();
                     Guitarist guitarist = new Guitarist();

// 一般情况下,随着类的承继和方法的重写
                     // 方法是越来越具体、越来越个性化
                     actor.DoShow();
                     bandsman.DoShow();
                     guitarist.DoShow();

Console.WriteLine("===========================");
                    
                     //尝试多态用法
                     Actor myActor1 = new Bandsman();               //正确:乐手是演员
                     Actor myActor2 = new Guitarist();                  //正确:吉他手是演员
                     Bandsman myBandsman = new Guitarist();   //正确:吉他手是乐手

// Look!!!

// 调用的是引用类型所引用的实例的方法

// 引用类型本身的函数是virtual的

// 看似"存在",实际已经被其子类重写(不是隐藏,而是被kill掉了)

// 这正是virtual所要表达的"名存实亡"的本意,而非一个"虚"字所能传达
                     myActor1.DoShow();
                     myActor2.DoShow();
                     myBandsman.DoShow();
              }
       }
}

代码分析:

1.        除了将继承链中最顶层基类的DoShow()方法改为用virtual修饰;把继承链中派生类的DoShow()方法改为override修饰以重写基类的方法。

2.        主程序代码没变,但下半部分产生的效果完全不同!请体会“引用变量本身方法”与“引用变量所引用实例的方法”的不同——这是关键。

多态成因的分析:

为什么会产生这样的效果呢?这里要提到一个“virtual表”的问题。我们看看程序中继承链的构成:Actor à Bandsman à Guitarist。因为派生类不但继承了基类的代码(确切地说是public代码)而且还有自己的特有代码(无论是不是与基类同名,都是自己特有的)。从程序的逻辑视角来看,你可以这样想象:在内存中,子类的实例所占的内存块是在父类所占的内存块的基础上“追加”了一小块——拜托大家自己画画图。这多出来的一小块里,装的就是子类特有的数据和代码。

我们仔细分析这几句代码:

1.        Actor actor = new Actor();        //常规的声明及分配内存方法
因为类是引用类型,所以actor这个引用变量是放在栈里的、类型是Actor类型,而它所引用的实例——同样也是Actor类型的——内存由new操作符来分配并且放在堆里。这样,引用变量与实例的类型一模一样、完全匹配。换句话说:栈里的引用变量所能“管理”的堆中的内存块大小正好、不多也不少。

2.        Actor myActor1 = new Bandsman();                     //正确:乐手是演员
同样是这句代码,在两个例子中产生的效果完全不同。为什么呢?且看!在例1中,在Bandsman类中只是使用new将父类的DoShow()给隐藏了——所起的作用仅限于自己对父类追加的代码块中,丝毫没有影响到父类。而栈中的引用变量是Actor类型的myActor1,它只能管理Actor类实例所占的那么大一块内存,而对追加的内存毫无控制能力(或者说看不见追加的这块内存)。因此,当你使用myActor1.DoShow();调用成员方法时,myActor1只能使唤自己能管到的那块内存里的DoShow()方法。那么例2中呢?难道例2中的myActor1就能管理追加的一块内存了吗?否也!它仍然管理不了,但不要忘了——这时候Actor类中的DoShow()方法已经被virtual所修饰,同时Bandsman类中的DoShow()方法已经被override修饰。这时候,当执行myActor1.DoShow();一句时,myActor1调用自己所管辖的内存块时,发现DoShow()这个函数已经标记为“可被重写”了(其实,在VB.NET中,与C#的virtual关键字对应的关键字就是Overridable,更直白),那么它就会尝试去发现有没有override链(也就是virtual表,即“虚表”) 的存在,如果存在,那么就调用override链上的最新可用版本——这就有了我们在例2中看到的效果。

3.        Actor myActor2 = new Guitarist();                  //正确:吉他手是演员
通过这句代码,你也可以想象一下2级重写是怎么形成的,同时也可以感悟一下所谓“重写链上最新的可用版本”是什么意思。

4.        Guitarist myActor2 = new Actor();                  //错误:想一想为什么?
呵呵,这是错误的,原因是引用变量所管理的内存大小超出了实例实际的内存大小。

乱弹:

多态,台湾的兄弟们喜欢称“多型”,一样的。“多”表示在实例化引用变量的时候,根据用户当时的使用情况(这时候程序已经Release了,不能再修改了,程序员已经不能控制程序了)智能地给出个性化的响应。

多,谓之变。莫非“多态”亦可称为“变态”耶?咦……“变型”……让我想起Transformer来了。

TO BE CONTINUE

下篇预告《深入浅出话多态(下)——牛刀小试》

法律声明:本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。若您需要转载,请务必注明文章出处为CSDN以保障网站的权益;请务必注明文章作者为刘铁猛,并向bladey@tom.com发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!

深入浅出话多态(上)——具体而微相关推荐

  1. 友元 java_深入浅出话友元

    深入浅出话友元 小序: 有一阵子没来打扫Blog了--技术这东西,就是走走停停的,一段时间就会遇到一个瓶颈.迷茫一下,然后发现与其因为迷茫而停滞不前,不如瞄准一个大方向勇敢地游下去. 这两天有几个正在 ...

  2. WPF学习之深入浅出话模板

    图形用户界面应用程序较之控制台界面应用程序最大的好处就是界面友好.数据显示直观.CUI程序中数据只能以文本的形式线性显示,GUI程序则允许数据以文本.列表.图形等多种形式立体显示. 用户体验在GUI程 ...

  3. 深入浅出话命令(Command)-笔记(-)

    深入浅出话命令(Command)-笔记(-) 一 基本概念 命令的基本元素: 命令(Command):实现了ICommand接口的类,平常使用最多的是RoutedCommand类. 命令源(Comma ...

  4. 深入浅出话VC++(2)——MFC的本质

    一.引言 上一专题中,纯手动地完成了一个Windows应用程序,然而,在实际开发中,我们大多数都是使用已有的类库来开发Windows应用程序.MFC(Microsoft Foundation Clas ...

  5. WPF学习之深入浅出话命令

    WPF为我们准备了完善的命令系统,你可能会问:"有了路由事件为什么还需要命令系统呢?".事件的作用是发布.传播一些消息,消息传达到了接收者,事件的指令也就算完成了,至于如何响应事件 ...

  6. 腾讯云大学大咖分享 | 深入浅出话智能语音识别

    语音识别就是把语音变成文字的过程,相信大家在平时生活也已经用到过一些语音识别的场景,比如说语音输入法.地图产品的语音输入.近年来,随着互联网的发展,各种音频数据和文本数据得到不断积累和丰富,CPU.G ...

  7. C++小工修炼之路XXI(多态上)

    什么是多态?你对多态的认识? 简单来说就是同一事物在不同的场景下表现出的多种形态. 栗子:网上的例子,见人说人或话,见鬼说鬼话. 一个人见到不同身份的人说话的态度不一样. 多态的实现条件: 在继承的体 ...

  8. WPF学习第九集-深入浅出话命令

    WPF为我们准备了完善的命令系统,你可能会问:"有了路由事件为什么还需要命令系统呢?".事件的作用是发布.传播一些消息,消息传达到了接收者,事件的指令也就算完成了,至于如何响应事件 ...

  9. 深入浅出Node.js(上)

    (一):什么是Node.js Node.js从2009年诞生至今,已经发展了两年有余,其成长的速度有目共睹.从在github的访问量超过Rails,到去年底Node.jsS创始人Ryan Dalh加盟 ...

最新文章

  1. java web 怎么用solr_使用web过滤器增加solr后台登录验证
  2. ASP.NET Core 中文文档 第二章 指南(4.4)添加 Model
  3. 四、物理数据模型PDM(Physical Data Model )
  4. ITK:在一幅图像中提取感兴趣区域ROI
  5. Django(part53)--404模板文件
  6. 基础功能2-python修改文件中所有文件名
  7. java九九乘法表右上三角_输出九九乘法表
  8. Spring Boot Mybatis入门示例
  9. nas服务器改成网站,nas配置web服务器
  10. CentOS上安装Git
  11. 颜色模型与颜色应用---标准基色和色度图
  12. 使用S32DS集成MCAL
  13. 2018 考研 408 经验贴——总结篇
  14. 词霸天下---192 词根【-oxy- = -oxia- = -oxic- 尖锐,敏锐,氧 】
  15. 终极.NET混淆器丨.NET Reactor产品介绍
  16. 初学C语言对于结构体变量名的一些想当然
  17. 【领域泛化论文阅读】Birds of A Feather Flock Together:Category-Divergence Guidance for DomainAdaptiveSegmentat
  18. 基于 Flutter 的 Web 渲染引擎「北海」正式开源!
  19. 《有一种错过叫作遗憾》
  20. Elasticsearch Ingest Pipeline

热门文章

  1. ABI Research产业研究:ZiFiSense如何革新物流货物及运输包装追踪
  2. 一个用于ocr中文本检测的数据集生成工具
  3. 绥化学院学报杂志绥化学院学杂志社绥化学院学报编辑部2022年第9期目录
  4. C语言实现连珠棋(三子棋、五子棋)
  5. 商家WIFI码项目全解析
  6. 在线文本实体抽取能力,助力应用解析海量文本数据
  7. 针对初学者的PID算法教程,以及趣味总结
  8. linux 串口 中断方式,设置在linux下串口中断(setting serial port interruption in
  9. 2023-05-20:go语言的slice和rust语言的Vec的扩容流程是什么?
  10. 三维计算机辅助设计教程,三维计算机辅助设计教程-Pro ENGINEER.pdf