深入浅出话多态(上)——具体而微
小序
前几天写了一篇《深入浅出话委托》,很多兄弟姐妹发
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.
“重写”、“覆盖”、“隐藏”是同一个概念吗?
今天我们学习多态,其实就是解决问题
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();
}
}
}
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 操作符来分配并且放在堆里。这样,引用变量与实例的类型一模一样、完全匹配。换句话说:栈里的引用变量所能“管理”的堆中的内存块大小正好、不多也不少。
因为类是引用类型,所以 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 中看到的效果。
同样是这句代码,在两个例子中产生的效果完全不同。为什么呢?且看!在例 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 级重写是怎么形成的,同时也可以感悟一下所谓“重写链上最新的可用版本”是什么意思。
通过这句代码,你也可以想象一下 2 级重写是怎么形成的,同时也可以感悟一下所谓“重写链上最新的可用版本”是什么意思。
4.
Guitarist
myActor2 = new Actor(); //
错误:想一想为什么?
呵呵,这是错误的,原因是引用变量所管理的内存大小超出了实例实际的内存大小。
呵呵,这是错误的,原因是引用变量所管理的内存大小超出了实例实际的内存大小。