我們繼續探討 C# 3.0 如何透過擴展方法為介面提供預設實作,以模擬多重繼承的效果。這次我們測試當實作類別本身定義了與擴展方法同名的方法時,呼叫行為會有何變化。
public interface ITestA { }
public static class ITestAHelper
{
public static void TestA(this ITestA obj)
{
Console.WriteLine("ITestAHelper.TestA");
}
}
public interface ITestB { }
public static class ITestBHelper
{
public static void TestB(this ITestB obj)
{
Console.WriteLine("ITestBHelper.TestB");
}
}
public class Test : ITestA, ITestB
{
// 此方法與 ITestA 的 TestA() 擴展方法同名
public void TestA()
{
Console.WriteLine("Test.TestA");
}
}
class Program
{
static void Main(string[] args)
{
Test obj1 = new Test();
// 分別測試兩種 TestA() 呼叫方式
obj1.TestA();
((ITestA)obj1).TestA();
Console.ReadKey();
}
}
Test.TestA
ITestAHelper.TestA
從結果可以發現一個關鍵行為:當實作類別擁有同名實例方法時,該方法會隱藏介面的擴展方法。直接對實例呼叫 obj1.TestA() 會執行類別自身的方法;但若將實例轉型為介面 ((ITestA)obj1).TestA(),則會呼叫擴展方法。
這引出了一個問題:如何實現多態? 如果擴展方法總是被實作類別隱藏,我們就無法透過介面參考來動態調度不同的實作。
virtual 修飾符直覺上我們可能想將擴展方法標記為 virtual 以允許覆寫,但編譯器會報錯:
public static virtual void TestA(this ITestA obj) // 編譯錯誤:static 方法不能使用 virtual
{
Console.WriteLine("ITestAHelper.TestA");
}
靜態方法不支援 virtual、override 或 abstract,這條路走不通。
我們修改介面 ITestA,加入 void TestA(); 宣告:
public interface ITestA
{
void TestA();
}
public static class ITestAHelper
{
public static void TestA(this ITestA obj)
{
Console.WriteLine("ITestAHelper.TestA");
}
}
Test.TestA
Test.TestA
這時候,擴展方法完全被實作類別的方法「藏」起來了,即使透過介面參考呼叫,也會執行實作類別的方法。擴展方法似乎永遠無法被介面變數存取。或許用反射可以繞過?但這已經超出了正常語法的範疇,我們暫不深究。
測試結果是編譯錯誤。編譯器無法決定該呼叫哪個擴展方法,導致歧義。感興趣的讀者可以自行嘗試驗證。
virtual,導致擴展方法只能被實作類別隱藏,無法被覆寫,也就無法實現真正的多態。這是一個較大的限制。若未來語言層面能解決此限制(例如允許介面預設實作),將會更完美。但正如俗話所說:「這個世界沒有完美的東西。」