從輸出結果中,我們可以看到,雖然 Derived2.PrintStatus 應用了 new,但卻依然參與動態綁定,這是由於 new 只能割斷 Derived2.PrintStatus 和 Base.PrintStatus 的聯系,而不能割斷它與 IFace.PrintStatus 的聯系。我在 Derived2 的定義中重新指定實現 IFace,這將使得編譯器認為 Derived2.PrintStatus 是 IFace.PrintStatus 的隱式實現,於是,在動態綁定時 Derived2.PrintStatus 就被包括進來了。
5. 誰的問題?
我必須指出,如果 Base(Code #01)和 Derived2(Code #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 的設計者已經預料到這類情況的發生,所以 CanRead、CanSeek 和 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)來糾正非預期的多態行為。
6. 最後...
當我的朋友拿著問題來找我時,我通常都不會直接給出我的答案,而是盡我的能力向他提供足夠多的可用信息,以便他能夠根據他所面臨的實際情況作出處理,畢竟,我不會比他更了解他的問題,而他也應該形成他自己的關於他的問題的思考。我希望浪子能用自己的答案回答他所提出的問題,因為只有這樣,那些知識才真正屬於他,並且我也相信本文已經提供了足夠多的可用信息。