C#中多態的使用

1. 你通常怎樣用多態?

     
假設我有一個類,裏面有一個 PrintStatus 方法,用於打印實例的當前狀態,我希望該類的派生類都帶有一個 PrintStatus 方法,並且這些方法都用於打印其實例的當前狀態。那麼我會這樣表達我的願望:

//  Code #01
class  Base
{
    public  virtual  void  PrintStatus()
    {
        Console.WriteLine( " public virtual void PrintStatus() in Base " );
    }
}
於是我可以寫一個這樣的方法:

//  Code #02
public  void  DisplayStatusOf(Base[] bs)
{
    foreach  (Base b  in  bs)
    {
        b.PrintStatus();
    }
}
bs
中可能包含着不同的 Base 的派生類,但我們卻可以忽略這些個性而使用一種統一的方式來處理某事。在 .NET 2.0 中,XmlReader Create 有這樣一個版本:
public  static  XmlReader Create(Stream input);
你可以向 Create 傳遞任何可用的,例如來自文件的FileStream)、來自內存的MemoryStream)或來自網絡的NetworkStream)等。雖然每一中的工作細節都不同,但我們卻使用一種統一的方式來處理這些

2. 假如有人不遵守承諾...

     DisplayStatusOf
隱含着這樣一個假設:bs 中如果存在派生類的實例,那麼該派生類應該重寫 PrintStatus,當然必須加上 override 關鍵字:

//  Code #03
class  Derived1 : Base
{
    public  override  void  PrintStatus( )

    {
        Console.WriteLine( " public override void PrintStatus() in Derived1 " );
    }

你可以把這看作一種承諾、約定,直到有人沉不住氣...

//  Code #04
class  Derived2 : Base
{
    public  new  void  PrintStatus()
    {
        Console.WriteLine( " public new void PrintStatus() in Derived2 " );
    }
}
假設我們有這樣一個數組:

//  Code #05
Base[] bs  =  new  Base[]
{
    new  Base(),
    new  Derived1(),
    new  Derived2()
} ;
把它傳遞給 DisplayStatusOf,則輸出是:

//  Output #01

//  public virtual void PrintStatus() in Base
//  public override void PrintStatus() in Derived1
//  public virtual void PrintStatus() in Base
從輸出結果中很容易看出 Derived2 並沒有按照我們期望的去做。但你無需驚訝,這是由於 Derived2 的設計者沒有遵守約定的緣故。



3. new
:封印咒術

      new
似乎給人一種這樣的感覺,它的使用者喜歡打破別人的約定,然而,如果使用恰當,new 可以彌補基類設計者的短見

      Output #01 中我們可以看到,new 只是把 Base.PrintStatus 封印起來而不是消滅掉,你可以解除封印然後進行訪問。對於 Derived2 的使用者,解封的方法是把 Derived2 的實例轉換成 Base 類型:

//  Code #06
Base d2  =  new  Derived2();
d2.PrintStatus();

//  Output #02
//  public virtual void PrintStatus() in Base
而在 Derived2 內部,你可以透過 base 來訪問:

//  Code #07

base .PrintStatus();
這種方法是針對實例成員的,如果被封印的成員是靜態成員的話,就要透過類名來訪問了。



4.
假如 Base.PrintStatus 是某個接口的隱式實現...

     
假如 Base 實現了一個 IFace 接口:

//  Code #08
interface  IFace
{
    void  PrintStatus();
}

class  Base : IFace
{
    public  virtual  void  PrintStatus()
    {
        Console.WriteLine( " public virtual void PrintStatus() in Base " );
    }
}
我們只需要讓 Derived2 重新實現 IFace

//  Code #09
class  Derived2 : Base, IFace
{
    public  new  void  PrintStatus()
    {
        Console.WriteLine( " public new void PrintStatus() in Derived2 " );
    }
}
Derived1
保持不變。則把:

//  Code #10

IFace[] fs  =  new  IFace[]
{
    new  Base(),
    new  Derived1(),
    new  Derived2(),
}
傳遞給:
//  Code #11
public  void  DisplayStatusOf(IFace[] fs)
{
    foreach  (IFace f  in  fs)
    {
        f.PrintStatus();
    }
}
的輸出結果是:

//  Output #03

//  public virtual void PrintStatus() in Base
//  public override void PrintStatus() in Derived1
//  public new void PrintStatus() in Derived2
     
從輸出結果中,我們可以看到,雖然 Derived2.PrintStatus 應用了 new,但卻依然參與動態綁定,這是由於 new 只能割斷 Derived2.PrintStatus Base.PrintStatus 的聯繫,而不能割斷它與 IFace.PrintStatus 的聯繫。我在 Derived2 的定義中重新指定實現 IFace,這將使得編譯器認爲 Derived2.PrintStatus IFace.PrintStatus 的隱式實現,於是,在動態綁定時 Derived2.PrintStatus 就被包括進來了。



5. 誰的問題?

     
我必須指出,如果 BaseCode #01)和 Derived2Code #04)同時存在的話,它們倆其中一個存在着設計上的問題。爲什麼這樣說呢?Base 的設計者在 PrintStatus 上應用 virtual 說明了他希望派生類能透過重寫這一方法來參與動態綁定,即多態性;而 Derived2 的設計者在 PrintStatus 上應用 new 則說明了他希望割斷 Derived2.PrintStatus Base.PrintStatus 之間的聯繫,這將使得 Derived2.PrintStatus 無法參與到 Base 的設計者所期望的動態綁定中。如果在 Base.PrintStatus 上應用 virtual(即對多態性的期望)是合理的話,那麼 Derived2.PrintStatus 應該換用另外一個名字了;如果在 Derived2.PrintStatus 上應用 new(即否決參與動態綁定)是合理的,那麼 Base.PrintStatus 應該考慮是否去掉 virtual 了,否則就會出現一些奇怪的行爲,例如 Output #01 的第三行輸出。

     
假如繼承體系中多態性行爲的期望是合理的話,那麼更實際的做法應該是把 Base 定義成這樣:

//  Code #12

abstract  class  Base
{
    public  abstract  void  PrintStatus();
}
而原來 Base 中的實現應該下移到一個派生類中:

//  Code #13
class  Derived3 : Base
{
    public  override  void  PrintStatus()
    {
        Console.WriteLine( " public override void PrintStatus() in Derived3 [originally implemented in Base] " );
    }
}
這樣,Derived2.PrintStatus 將使得編譯無法完成,從而迫使其設計者要麼更改方法的名字,要麼換用 override 修飾。這種強制使得 Derived2 的設計者不得不重新考慮其設計的合理性。

     
假如繼承體系中多態性行爲的期望不總是合理呢?例如 Stream 有這樣一個方法:

public  abstract  long  Seek( long  offset, SeekOrigin origin);
現在假設我有一個方法在處理輸入流時需要用到 Stream.Seek

//  Code #14
public  void  Resume(Stream input,  long  offset)
{
    input.Seek(offset, SeekOrigin.Begin);
}
當我們向 Resume 傳遞一個 NetworkStream 的實例,Resume 將會拋出一個 NotSupportedException,因爲 NetworkStream 不支持 Seek那麼這是否說明 Stream 的設計有問題呢?

設想 Resume 是一個下載工具進行斷點續傳的方法,然而,並不是所有的服務器都支持斷點續傳的,於是,你需要首先判斷輸入流是否支持 Seek 操作,再決定如何處理輸入流:

//  Code #15
public  void  Resume(Stream input,  long  offset)
{
    if  (input.CanSeek)
    {
        input.Seek(offset, SeekOrigin.Begin);
    }
    else
    {
        // 
    }
}
如果 CanSeek false,那就只好從頭來過了。

     
實際上,我們並不能保證任何 Stream 的派生類都能夠支持某個(些)操作,我們甚至不能保證來自同一個派生類的所有實例都支持某個(些)操作。你可以設想有這樣一個 PriorityStream,它能夠根據當前登錄賬號的權限來決定是否提供寫操作,這使得擁有足夠權限的人才能修改數據。或許 Stream 的設計者已經預料到這類情況的發生,所以 CanReadCanSeek CanWrite 就被加入到 Stream 裏了。

     
值得注意的是,Code #07 Derived2 可能是一個很糟糕的設計,也可能是一個很實用的設計。在本文,它是一個很糟糕的設計,如果你足夠細心,你會察覺到 Derived2 的設計者希望 Derived2.PrintStatus 繞過 Base.PrintStatus 而直接和 IFace.PrintStauts 進行關聯,表面上這沒什麼不妥,但實質上 Base.PrintStatus IFace.PrintStauts 在約定上是同質的,這意味着如果與 IFace.PrintStauts 進行關聯就等於承認自己和 Base.PrintStatus 是同質的,這樣的話,爲什麼不直接在 Derived2 裏重寫 PrintStatus 呢?在《基類與接口混合繼承的聲明問題》中,我示範了一個實用的設計,用 new 和接口重新實現(Interface reimplementation)來糾正非預期的多態行爲。

 
發佈了20 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章