深刻淺出話多態(上)——具體而微

小序
       前幾天寫了一篇《深刻淺出話委託》,不少兄弟姐妹發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給我( [email]bladey@tom.com[/email] ),也許你能收到一份薪水和福利都不錯的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程序

//                          水之真諦                                     //
//            [url]http://blog.csdn.net/FantasiaX[/url]               //
//                   上善若水,潤物無聲                             //
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,真正的多態

//                          水之真諦                                     //
//            [url]http://blog.csdn.net/FantasiaX[/url]               //
//                   上善若水,潤物無聲                             //
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
下篇預告《深刻淺出話多態(下)——牛刀小試》
相關文章
相關標籤/搜索