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

来源:互联网 发布:全息投影淘宝 编辑:程序博客网 时间:2024/06/05 14:24
2006年06月29日 11:19:00

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

小序

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

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

正文

一.什么是多态(Polymorphism

多态(Polymorphism)是面向对象(Object-OrientedOO)思想"三大特征"之一,其余两个分别是封装(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:灵长类动物。此类实现了13功能集合。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1. 如何使用virtualoverride

2. 如何使用abstractoverride

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. 主程序代码的下半部分已经算是多态了,但由于没有使用virtualoverride,多态最有价值的效果--个性化方法实现--没有体现出来。后面的例子专门体现这一点。

例子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发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=849820


原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 如果孩子考不好怎么办 6岁不认识数字怎么办 数学一点都不会怎么办 初一数学太差怎么办 三年级孩子数学差怎么办 三年级孩子数学很差怎么办 初中学习不好高中怎么办 四年级孩子数学不好怎么办 孩子学习不开窍怎么办 孩子学习太笨怎么办 老师是个小人怎么办 孩子写字太差怎么办 孩子写字下手重怎么办 孩子一年级数学不好怎么办 孩子数学理解能力差怎么办 智商情商都低怎么办 一年级孩子数学很差怎么办 一年级孩子数学差怎么办 一年级数学学不好怎么办 孩子成绩差该怎么办 小学生数学太差怎么办 小学数学基础差怎么办 孩子一年级学习不好怎么办 快两岁的宝宝老尿裤怎么办 戒母乳宝宝哭闹怎么办 三周岁不肯说话怎么办 两岁宝宝打人怎么办 刚开始跳绳腿疼怎么办 两周岁宝宝拉肚子怎么办 宝宝睡觉认人怎么办 宝宝脸不光滑怎么办 两周岁宝宝打人怎么办 分手后想念前任怎么办 孩子不学习该怎么办 小孩吃了牙膏怎么办 小孩子吃了牙膏怎么办 一岁宝宝龋齿怎么办 宝宝吃牙膏了怎么办 刷牙吞了牙膏怎么办 宝宝刷牙吞牙膏怎么办 宝宝语言发育迟缓怎么办