上篇----具体而微
小序
前几天写了一篇《深入浅出话委托》,很多兄弟姐妹发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来了。
下篇----牛刀小试
小序
英格兰走了……巴西的表演还没有开场。闲着也是闲着,我把下篇写出来。
正文
一.多态的现实意义
如果一个编程元素没有可以应用在软件工程中的现实意义,那将是一件不可容忍的事情。同理,如果你不了解一个编程元素的现实意义、不知道在编程时应该怎么用,就不能说自己懂得了这个编程元素。
我的编程经验实在不多,就我个人感觉,多态最大的现实意义在于“代码的简化”。
多态为什么能简化代码捏?
先让我们用一句话概括多态的实现:首先要一个人父类,在这个父类的成员中,有一个virtual的(可以被子类重写的)方法。然后,有N多子类继承了这个父类,并且用override重写了父类的那个virtual方法——此时已经形成了一个扇形的多态继承图。当然,如果用作“父类”的是一个接口,那么在子类中就不是“重写”方法,而是“实现”方法了。
一旦这个“继承扇”形成了,我们应该意识到——无论是父类还是子类,他们都有一个同名的方法,而且此同名方法在各个子类中是“个性化”的——它重写了父类的方法、并且子类与子类之间的这个同名方法也各不相同。
在程序的编写期,程序员总要预期用户可能进行的各种操作——比如对“继承扇”中每个子类的操作。程序编译完成、成为可执行文件并交付用户后,程序员就不能再控制程序了,这时候程序只能听从用户的摆布。假设没有多态,那么为了让用户在调用每个子类的时候程序都能有正确的响应,程序员不得不为每个子类在内存中创建一个实例——这样一来,程序复杂度增加的同时,性能也下降了。还好,这只是个假设……
OK,让我们还是来拿代码说事儿吧。下面给出两段代码,对比显示了多态的巨大优越性。
代码1:非多态排比代码
// 水之真谛 //
// http://blog.csdn.net/FantasiaX //
// 上善若水,润物无声 //
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;
}
}
}
}
}
代码分析:
1. 一上来是4个独立的类(相信这4位人物大家都不陌生吧……),这4个类有一个同名方法:Transform()。虽然同名,但各自的实现却是“个性化”的、完全不同的——我们这里只用输出不同的字符串来表示,但你想啊——同样是有胳膊有腿的一个大家伙,变成汽车的方法跟变成飞机、变成枪怎么可能一样呢?
2. 进入主程序后,先是为每个类实例化一个对象出来,以备用户自由调用。这么做是很占内存的,如果为了优化程序,对每个类的实例化是可以挪到switch的每个case分支里的。
3. 一个无限循环,可以反复输入数字……
4. switch…case…根据用户的需求来调用合适的Transformer的Transform方法。
代码2:使用多态,简化代码
// 水之真谛 //
// http://blog.csdn.net/FantasiaX //
// 上善若水,润物无声 //
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 ??");
}
}
}
}
}
代码分析:
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吧!恭喜你,你上了一个大台阶!
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);
}
唔……窗体里有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需求:……
}
2. 将这个事件处理函数“挂接”在窗体的每个Control的MouseEvent事件上,方法是:在“属性”面板里切换到Control的“事件”页面(点那个小闪电),然后选中MouseEvent事件,再点击右边的向下箭头,在下拉菜单中会出现上面我们手写的函数——选中它。如图:
转自 http://blog.csdn.net/fantasiax/archive/2006/06/19/793623.aspx
3. 为每一个Control的MouseEvent事件挂接这个响应函数。一个简短的、可扩展的程序就完成了!
代码分析:
1. 函数之所以声明成:
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()。
3. 显式类型转换,为toolTip1的SetToolTip()函数准备一个参数:
Control currentControl = (Control)sender;
其实,这也是多态的体现:子类可以当作任意其父类使用。sender虽然是一个object类型的变量,但它实际上是指向内存中的一个具体的Control实例——MouseEnter事件的拥有者(比如一个Button的实例或者一个TextBox的实例)。而Button、TextBox等类的父类就是Control类,所以完全可以这么用。
4. 后面的代码就非常简单了,不说了。
至此,一个结构清晰,代码简单(只有原来的1/50长度,操作也为原来的1/20不到),便于维护而且扩展性极佳的程序就新鲜出炉了!没有多态,这是不可能实现的。
作业:
自己动手把这个WinForm程序完成,并且确保自己能够分析清楚每句代码的含意。
花絮:
现实当中,WinForm程序的一大部分代码都是由Visual Studio 2005为我们写好的,鳞次栉比、非常好看——但初学者常被搞的晕头转向。没别的办法:大胆打开那些代码、仔细察看、动手跟踪跟踪、修改修改——别怕出错!经验大都是从错误中萃取出来的精华,有时候几十个错误才能为你换来那么一丁点领悟。高手不仅仅是比我们看书多,更重要是犯的错比我们多,呵呵……
唉……长舒一口气。
最后我想说的是:要想读懂这些文章,首先要慢慢读——我写它的时候思路是清清楚楚的,但我的思想是我的思想,理解它的时候要一句一句看,说真的,错过一两个字都有可能读不懂。还有就是代码,一定要自己敲一遍。
如果大家有什么疑问,别客气,在后面跟帖发问就是了。只要有时间,我会一一作答。如果我有哪里写的不对,也请各位高手多多指教,我会立刻更正。
到此为止,这篇又臭又长的文章可以OVER了——砖我是抛出去了,等您的玉呢!
OVER