深刻淺出話多態(下)——牛刀小試
小序
英格蘭走了……巴西的表演尚未開場。閒着也是閒着,我把下篇寫出來。
正文
一.多態的現實意義
若是一個編程元素沒有能夠應用在軟件工程中的現實意義,那將是一件不可容忍的事情。同理,若是你不瞭解一個編程元素的現實意義、不知道在編程時應該怎麼用,就不能說本身懂得了這個編程元素。
個人編程經驗實在很少,就我我的感受,多態最大的現實意義在於「代碼的簡化」。
多態爲何能簡化代碼捏?
先讓咱們用一句話歸納多態的實現:首先要一我的父類,在這個父類的成員中,有一個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;
}
}
}
}
}
代碼分析:
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 ??");
}
}
}
}
}
代碼分析:
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事件,再點擊右邊的向下箭頭,在下拉菜單中會出現上面咱們手寫的函數——選中它。如圖:
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
下期預告:《深刻淺出話反射》