一.多态的现实意义
如果一个编程元素没有可以应用在软件工程中的现实意义,那将是一件不可容忍的事情。同理,如果你不了解一个编程元素的现实意义、不知道在编程时应该怎么用,就不能说自己懂得了这个编程元素。
我的编程经验实在不多,就我个人感觉,多态最大的现实意义在于“代码的简化”。
多态为什么能简化代码捏?
先让我们用一句话概括多态的实现:首先要一个人父类,在这个父类的成员中,有一个
virtual
的(可以被子类重写的)方法。然后,有
N
多子类继承了这个父类,并且用
override
重写了父类的那个
virtual
方法——此时已经形成了一个扇形的多态继承图。当然,如果用作“父类”的是一个接口,那么在子类中就不是“重写”方法,而是“实现”方法了。
一旦这个“继承扇”形成了,我们应该意识到——无论是父类还是子类,他们都有一个同名的方法,而且此同名方法在各个子类中是“个性化”的——它重写了父类的方法、并且子类与子类之间的这个同名方法也各不相同。
在程序的编写期,程序员总要预期用户可能进行的各种操作——比如对“继承扇”中每个子类的操作。程序编译完成、成为可执行文件并交付用户后,程序员就不能再控制程序了,这时候程序只能听从用户的摆布。假设没有多态,那么为了让用户在调用每个子类的时候程序都能有正确的响应,程序员不得不为每个子类在内存中创建一个实例——这样一来,程序复杂度增加的同时,性能也下降了。还好,这只是个假设……
OK
,让我们还是来拿代码说事儿吧。下面给出两段代码,对比显示了多态的巨大优越性。
代码
1
:
非多态排比代码
using
System;
using System.Collections.Generic;
using System.Text;
namespace Sample
{
class OptimusPrime // 博派老大擎天柱
{
public void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
class Megatron // 狂派老大威震天
{
public void Transform()
{
Console.WriteLine("Transform to a GUN...");
}
}
class Bumblebee // 大黄蜂
{
public void Transform()
{
Console.WriteLine("Transform to a CAR...");
}
}
class Starscream // 红蜘蛛
{
public void Transform()
{
Console.WriteLine("Transform to a FIGHTER...");
}
}
class Program // 主程序类
{
static void Main(string[] args)
{
string number = string.Empty;
// 为每个类准备一个实例
OptimusPrime transformer1 = new OptimusPrime();
Megatron transformer2 = new Megatron();
Bumblebee transformer3 = new Bumblebee();
Starscream transformer4 = new Starscream();
while (true) // 无限循环
{
Console.WriteLine("Please input 1/2/3/4 to choose a transformer...");
number = Console.ReadLine();
switch (number) // 根据用户选择,作出响应
{
case "1":
transformer1.Transform();
break;
case "2":
transformer2.Transform();
break;
case "3":
transformer3.Transform();
break;
case "4":
transformer4.Transform();
break;
default:
Console.WriteLine("Do you want a TRACTOR ??");
break;
}
}
}
}
}
代码分析:
using System.Collections.Generic;
using System.Text;
namespace Sample
{
class OptimusPrime // 博派老大擎天柱
{
public void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
class Megatron // 狂派老大威震天
{
public void Transform()
{
Console.WriteLine("Transform to a GUN...");
}
}
class Bumblebee // 大黄蜂
{
public void Transform()
{
Console.WriteLine("Transform to a CAR...");
}
}
class Starscream // 红蜘蛛
{
public void Transform()
{
Console.WriteLine("Transform to a FIGHTER...");
}
}
class Program // 主程序类
{
static void Main(string[] args)
{
string number = string.Empty;
// 为每个类准备一个实例
OptimusPrime transformer1 = new OptimusPrime();
Megatron transformer2 = new Megatron();
Bumblebee transformer3 = new Bumblebee();
Starscream transformer4 = new Starscream();
while (true) // 无限循环
{
Console.WriteLine("Please input 1/2/3/4 to choose a transformer...");
number = Console.ReadLine();
switch (number) // 根据用户选择,作出响应
{
case "1":
transformer1.Transform();
break;
case "2":
transformer2.Transform();
break;
case "3":
transformer3.Transform();
break;
case "4":
transformer4.Transform();
break;
default:
Console.WriteLine("Do you want a TRACTOR ??");
break;
}
}
}
}
}
代码分析:
1.
一上来是
4
个独立的类(相信这
4
位人物大家都不陌生吧……),这
4
个类有一个同名方法:
Transform()
。虽然同名,但各自的实现却是“个性化”的、完全不同的——我们这里只用输出不同的字符串来表示,但你想啊——同样是有胳膊有腿的一个大家伙,变成汽车的方法跟变成飞机、变成枪怎么可能一样呢?
2.
进入主程序后,先是为每个类实例化一个对象出来,以备用户自由调用。这么做是很占内存的,如果为了优化程序,对每个类的实例化是可以挪到
switch
的每个
case
分支里的。
3.
一个无限循环,可以反复输入数字……
4.
switch…case…
根据用户的需求来调用合适的
Transformer
的
Transform
方法。
代码
2
:
使用多态,简化代码
using
System;
using System.Collections.Generic;
using System.Text;
namespace Sample
{
class Transformer // 基类
{
public virtual void Transform()
{
Console.WriteLine("Transform to a ??? ???...");
}
}
class OptimusPrime : Transformer // 博派老大擎天柱
{
public override void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
class Megatron : Transformer // 狂派老大威震天
{
public override void Transform()
{
Console.WriteLine("Transform to a GUN...");
}
}
class Bumblebee : Transformer // 大黄蜂
{
public override void Transform()
{
Console.WriteLine("Transform to a CAR...");
}
}
class Starscream : Transformer // 红蜘蛛
{
public override void Transform()
{
Console.WriteLine("Transform to a FIGHTER...");
}
}
class Program // 主程序类
{
static void Main(string[] args)
{
string number = string.Empty;
// 只准备一个变量即可,并且不用实例化
Transformer transformer;
while (true) // 无限循环
{
Console.WriteLine("Please input 1/2/3/4 to choose a transformer...");
number = Console.ReadLine();
switch (number) // 根据用户选择,作出响应,运行期 " 动态 " 实例化
{
case "1":
transformer = new OptimusPrime();
break;
case "2":
transformer = new Megatron();
break;
case "3":
transformer = new Bumblebee();
break;
case "4":
transformer = new Starscream();
break;
default:
transformer = null;
break;
}
if (transformer != null) // 这里是本程序的核心
{
transformer.Transform();
}
else
{
Console.WriteLine("Do you want a TRACTOR ??");
}
}
}
}
}
代码分析:
using System.Collections.Generic;
using System.Text;
namespace Sample
{
class Transformer // 基类
{
public virtual void Transform()
{
Console.WriteLine("Transform to a ??? ???...");
}
}
class OptimusPrime : Transformer // 博派老大擎天柱
{
public override void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
class Megatron : Transformer // 狂派老大威震天
{
public override void Transform()
{
Console.WriteLine("Transform to a GUN...");
}
}
class Bumblebee : Transformer // 大黄蜂
{
public override void Transform()
{
Console.WriteLine("Transform to a CAR...");
}
}
class Starscream : Transformer // 红蜘蛛
{
public override void Transform()
{
Console.WriteLine("Transform to a FIGHTER...");
}
}
class Program // 主程序类
{
static void Main(string[] args)
{
string number = string.Empty;
// 只准备一个变量即可,并且不用实例化
Transformer transformer;
while (true) // 无限循环
{
Console.WriteLine("Please input 1/2/3/4 to choose a transformer...");
number = Console.ReadLine();
switch (number) // 根据用户选择,作出响应,运行期 " 动态 " 实例化
{
case "1":
transformer = new OptimusPrime();
break;
case "2":
transformer = new Megatron();
break;
case "3":
transformer = new Bumblebee();
break;
case "4":
transformer = new Starscream();
break;
default:
transformer = null;
break;
}
if (transformer != null) // 这里是本程序的核心
{
transformer.Transform();
}
else
{
Console.WriteLine("Do you want a TRACTOR ??");
}
}
}
}
}
代码分析:
1.
为了展示多态效果,先装备了一个基类。这个基类是一个常规的类——可以实例化、调用其方法。不过,使用抽象类或者接口来展示多态效果也完全没有问题,因此,你把
Transformer
类替换成下面两种形式也是可以的:
( A )以抽象类做基类
abstract class Transformer // 基类,这是一个抽象类,方法只有声明没有实现
{
abstract public void Transform();
}
( B )以接口做基类
interface Transformer // 基接口,方法只有声明没有实现
{
void Transform();
}
注意:如果使用的是基接口而不是基类,那么实现基接口的时候,方法不再需要 override 关键字。 原因很简单,接口中的方法是没有“实现”的,所以只需要“新写”就可以了、不用“重写”。如下:
class OptimusPrime : Transformer // 博派老大擎天柱
{
public void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
花絮:
记得有一次公开课上,一个兄弟问我:到底是用常规类合适,还是用抽象类或者用基接口合适呢?如何判断、如何在工程中应用呢?我感觉这个问题问的非常好——这个已经不是 C# 语言所研究的范畴了,而是软件工程的范畴,确切地说,这是“设计模式”( Design Pattern )的范畴。当你已经对类的封装、继承、多态了如指掌后,不满足于这种小儿科的例子程序而打算用这些知识写出漂亮的软件工程时,那么你会发现:如何设计类、什么应该被设计成类、什么不应该,与耦合,类与类之间如何继承最稳定、最高效,类与类之间如何平衡内聚……这些问题都非常重要却又让你感觉无从下手。那么 OK ,去看《设计模式》吧,去学习 UML 吧!恭喜你,你上了一个大台阶!
( A )以抽象类做基类
abstract class Transformer // 基类,这是一个抽象类,方法只有声明没有实现
{
abstract public void Transform();
}
( B )以接口做基类
interface Transformer // 基接口,方法只有声明没有实现
{
void Transform();
}
注意:如果使用的是基接口而不是基类,那么实现基接口的时候,方法不再需要 override 关键字。 原因很简单,接口中的方法是没有“实现”的,所以只需要“新写”就可以了、不用“重写”。如下:
class OptimusPrime : Transformer // 博派老大擎天柱
{
public void Transform()
{
Console.WriteLine("Transform to a TRUCK...");
}
}
花絮:
记得有一次公开课上,一个兄弟问我:到底是用常规类合适,还是用抽象类或者用基接口合适呢?如何判断、如何在工程中应用呢?我感觉这个问题问的非常好——这个已经不是 C# 语言所研究的范畴了,而是软件工程的范畴,确切地说,这是“设计模式”( Design Pattern )的范畴。当你已经对类的封装、继承、多态了如指掌后,不满足于这种小儿科的例子程序而打算用这些知识写出漂亮的软件工程时,那么你会发现:如何设计类、什么应该被设计成类、什么不应该,与耦合,类与类之间如何继承最稳定、最高效,类与类之间如何平衡内聚……这些问题都非常重要却又让你感觉无从下手。那么 OK ,去看《设计模式》吧,去学习 UML 吧!恭喜你,你上了一个大台阶!
2.
在本例中,只声明了一个多态变量,而且没有实例化。实例化的步骤挪到
case
分支里去了。我记得有的书里管这样的形式叫“动态实例化”。
3.
switch…case…
分支中,根据用户的选择实例化
transform
引用变量。
4.
最后的
if…else…
是程序的核心。在
if
的
true
分支里,因为各类的
Transform()
方法是同名而且是
virtual/override
继承的,所以能够体现出多态性。
二.牛刀小试
光写上面那种没什么实际用处的例子程序还真没多大意思,也就唬弄唬弄新手、讲讲原理还行。下面这个例子是多态在实际应用中的一个例子。这个例子在《深入浅出话事件》里提到过,是有关于
WinForm
程序的事件中那个
object
类型的
sender
的。
OK
,让我们来考虑这样一种情况:我在窗体上放置了
50
个
Control
和一个
ToolTip
。现在要求当鼠标指向某一个
Control
的时候,
ToolTip
要显示当前所指
Control
的全名(
Full Name
)。
呵呵,这个听起来并不难,对吧!你可能会这样想:
1.
获得当前
Control
的名字,可以用这个
Control
的
ToString()
方法。
2.
在每个
Control
的
MouseEnter
事件里,让
ToolTip
显示上一步所获得的字符串就
OK
了。
3.
比如
button1
的
MouseEnter
事件响应函数写出来就应该是这样的:
private void button1_MouseEnter(object sender, EventArgs e)
{
string fullName = button1.ToString();
toolTip1.SetToolTip(button1, fullName);
}
而 comboBox1 的 MouseEnter 事件响应函数则是:
private void comboBox1_MouseEnter(object sender, EventArgs e)
{
string fullName = comboBox1.ToString();
toolTip1.SetToolTip(comboBox1, fullName);
}
private void button1_MouseEnter(object sender, EventArgs e)
{
string fullName = button1.ToString();
toolTip1.SetToolTip(button1, fullName);
}
而 comboBox1 的 MouseEnter 事件响应函数则是:
private void comboBox1_MouseEnter(object sender, EventArgs e)
{
string fullName = comboBox1.ToString();
toolTip1.SetToolTip(comboBox1, fullName);
}
唔……窗体里有
50
个
Control
,你怎么办呢?
噢!你说可以用“复制
/
粘贴”然后再改动两处代码?
真是个好主意!!不过,你打算在什么地方出错呢?这种“看起来一样”的复制
+
粘贴,是
Bug
的一大来源。况且我这里只有
50
个
Control
,如果是
500
个,你打算怎么办呢?
佛说:前世的
500
次回眸,换得今生的
1
次擦肩而过;可他没说今生的
500
次
Ctrl+C/Ctrl+V
能在下辈子奖你个鼠标啊
:P
OK
,我知道你有决心和毅力去仔细完成
50
次正确的
Ctrl+C/Ctrl+V
并且把两处代码都正确改完。当你完成这一切之后,市场部的兄弟告诉我们——客户的需求升级了!客户要求在鼠标移向某个
Control
时不但要显示它的
Full Name
,而且
ToolTip
的颜色是随机的!!
>_<
……
@#^&(#$%^@#
50
处的修改,不要漏掉喔……程序员吐吧吐吧,不是罪!
……拜
OO
之神所赐,我们有多态……噩梦结束了。让我们看看多态是如何简化代码、增强可扩展性的。
首先打开
MSDN
,我要
Show
你一点东西。请你分别查找
Button
类、
TextBox
类、
ListBox
类、
ComboBox
类……它们的
ToString()
方法。是不是都可以看到这样一句注释:
l
ToString
Overridden.
(这是
Button
类的,是重写了父类的。)
l
ToString
Returns a string that represents the TextBoxBase control. (Inherited from TextBoxBase.)
(这是
TextBox
的,说是从
TextBoxBase
继承来的,我们追查一下。)
l
ToString
Overridden. Returns a string that represents the TextBoxBase control.
(这是
TextBoxBase
的,也是重写了父类的。
TextBox
继承了它,所以仍然是重写的。)
l
ToString
Overridden. Returns a string representation of the ListBox.
(这是
ListBox
的,也明确指出是重写的。)
……
这些
Control
都是重写的谁的
ToString()
方法呢?其中的细节我就不说了——这这个重写链的最顶端,是“万类之源”——
Object
类。也就是说,在
Object
类中,就已经包含了这个
ToString()
方法。
Object
类在
C#
中正好对应
object
这个
Keyword
。
一切问题都解决了!让我们用多态来重构前面的代码!
1.
手动书写(或者改造某个
Control
的
MouseEnter
响应函数),成为如下代码:
private void common_MouseEnter(object sender, EventArgs e)
{
// 用户的第一需求:显示 ToolTip
string fullName = sender.ToString();
Control currentControl = (Control)sender;
toolTip1.SetToolTip(currentControl, fullName);
// 用户的第二需求:随机颜色
Color[] backColors = new Color[] { Color.CornflowerBlue, Color.Pink, Color.Orange };
Random r = new Random();
int i = r.Next(0, 3);
toolTip1.BackColor = backColors[i];
// 用户的第 N 需求: ……
}
private void common_MouseEnter(object sender, EventArgs e)
{
// 用户的第一需求:显示 ToolTip
string fullName = sender.ToString();
Control currentControl = (Control)sender;
toolTip1.SetToolTip(currentControl, fullName);
// 用户的第二需求:随机颜色
Color[] backColors = new Color[] { Color.CornflowerBlue, Color.Pink, Color.Orange };
Random r = new Random();
int i = r.Next(0, 3);
toolTip1.BackColor = backColors[i];
// 用户的第 N 需求: ……
}
2.
将这个事件处理函数“挂接”在窗体的每个
Control
的
MouseEvent
事件上,方法是:在“属性”面板里切换到
Control
的“事件”页面(点那个小闪电),然后选中
MouseEvent
事件,再点击右边的向下箭头,在下拉菜单中会出现上面我们手写的函数——选中它。如图:
3.
为每一个
Control
的
MouseEvent
事件挂接这个响应函数。一个简短的、可扩展的程序就完成了!
代码分析:
1.
函数之所以声明成:
private void common_MouseEnter(object sender, EventArgs e){…}
是为了与各 Control 的 MouseEnter 事件的委托相匹配。如果你不明白为什么这样做,请仔细阅读《深入浅出话事件》的上下两篇。
private void common_MouseEnter(object sender, EventArgs e){…}
是为了与各 Control 的 MouseEnter 事件的委托相匹配。如果你不明白为什么这样做,请仔细阅读《深入浅出话事件》的上下两篇。
2.
核心代码:
string fullName = sender.ToString();
体现了多态。看似是 sender 的 ToString() 方法,但由于各个类在激发事件的时候,实际上是以 this 的身份来发送消息的, this 在内存中指代的就是一个具体的 Control ——如果是 button1 发送的消息,那么这个 this 在内存中就是指向的 button1 ,只不过指向这块内存的引用变量是一个 object 类型的变量——典型的多态。又因为 Button 类继承自 Object 类,并且重写了 Object 的 ToString() 函数,所以在这里调用 sender.ToString() 实际上就调用了 button1.ToString() 。
string fullName = sender.ToString();
体现了多态。看似是 sender 的 ToString() 方法,但由于各个类在激发事件的时候,实际上是以 this 的身份来发送消息的, this 在内存中指代的就是一个具体的 Control ——如果是 button1 发送的消息,那么这个 this 在内存中就是指向的 button1 ,只不过指向这块内存的引用变量是一个 object 类型的变量——典型的多态。又因为 Button 类继承自 Object 类,并且重写了 Object 的 ToString() 函数,所以在这里调用 sender.ToString() 实际上就调用了 button1.ToString() 。
3.
显式类型转换,为
toolTip1
的
SetToolTip()
函数准备一个参数:
Control currentControl = (Control)sender;
其实,这也是多态的体现:子类可以当作任意其父类使用。 sender 虽然是一个 object 类型的变量,但它实际上是指向内存中的一个具体的 Control 实例—— MouseEnter 事件的拥有者(比如一个 Button 的实例或者一个 TextBox 的实例)。而 Button 、 TextBox 等类的父类就是 Control 类,所以完全可以这么用。
Control currentControl = (Control)sender;
其实,这也是多态的体现:子类可以当作任意其父类使用。 sender 虽然是一个 object 类型的变量,但它实际上是指向内存中的一个具体的 Control 实例—— MouseEnter 事件的拥有者(比如一个 Button 的实例或者一个 TextBox 的实例)。而 Button 、 TextBox 等类的父类就是 Control 类,所以完全可以这么用。
4.
后面的代码就非常简单了,不说了。
至此,一个结构清晰,代码简单(只有原来的1/50长度,操作也为原来的1/20不到),便于维护而且扩展性极佳的程序就新鲜出炉了!没有多态,这是不可能实现的。
作业:
自己动手把这个
WinForm
程序完成,并且确保自己能够分析清楚每句代码的含意。
花絮:
现实当中, WinForm 程序的一大部分代码都是由 Visual Studio 2005 为我们写好的,鳞次栉比、非常好看——但初学者常被搞的晕头转向。没别的办法:大胆打开那些代码、仔细察看、动手跟踪跟踪、修改修改——别怕出错!经验大都是从错误中萃取出来的精华,有时候几十个错误才能为你换来那么一丁点领悟。高手不仅仅是比我们看书多,更重要是犯的错比我们多,呵呵……
现实当中, WinForm 程序的一大部分代码都是由 Visual Studio 2005 为我们写好的,鳞次栉比、非常好看——但初学者常被搞的晕头转向。没别的办法:大胆打开那些代码、仔细察看、动手跟踪跟踪、修改修改——别怕出错!经验大都是从错误中萃取出来的精华,有时候几十个错误才能为你换来那么一丁点领悟。高手不仅仅是比我们看书多,更重要是犯的错比我们多,呵呵……
唉……长舒一口气。
最后我想说的是:要想读懂这些文章,首先要慢慢读——我写它的时候思路是清清楚楚的,但我的思想是我的思想,理解它的时候要一句一句看,说真的,错过一两个字都有可能读不懂。还有就是代码,一定要自己敲一遍。
如果大家有什么疑问,别客气,在后面跟帖发问就是了。只要有时间,我会一一作答。如果我有哪里写的不对,也请各位高手多多指教,我会立刻更正。
到此为止,这篇又臭又长的文章可以
OVER
了——砖我是抛出去了,等您的玉呢!
OVER
下期预告:《深入浅出话反射》
法律声明:
本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。若您需要转载,请务必注明文章出处为
CSDN
以保障网站的权益;请务必注明文章作者为
刘铁猛
,并向
bladey@tom.com
发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!