1. 問題初現
今天,查看《接口繼承的聲明問題》一文的反饋,發現Ninputer留下這樣一道題:
如果有
class A : Interface1
那麼
class B : A, Inteface1
和
class B : A
會出現什麼不同的情況呢。編譯器在IL級別是用什麼手段實現這個功能的呢?
2. 探索問題 & 理解問題
解決問題的過程既是一個探索的過程也是一個推理論證的過程。OK,下面我嘗試用反證法來探索這個問題。
首先,我假設問題中B類的兩種繼承方式有著一樣的效果,並試著尋找它們的不一樣。為了了解這兩種方式的效果,我把上面代碼補充完整:
interface IC { }
class A : IC { }
class B1 : A { }
class B2 : A, IC { }
class Program
{
static void Main()
{
A a = new A();
B1 b1 = new B1();
B2 b2 = new B2();
Console.WriteLine(a is IC);
Console.WriteLine(b1 is A);
Console.WriteLine(b1 is IC);
Console.WriteLine(b2 is A);
Console.WriteLine(b2 is IC);
}
}
代碼運行的結果是:
True
True
True
True
True
我們對此結果毫無疑問,那麼這是否代表著B1和B2之間沒有區別?如果上面的代碼作為推理前提在客觀上已經足夠充分,那麼答案是肯定的。但我無法知道論據是否已經達到充分的程度。於是,我把上面的代碼修改一下,為類和接口其添加一些成員並觀察一下它們所表現出來的行為:
interface IC
{
void M();
}
class A : IC
{
void IC.M()
{
Console.WriteLine("In class A");
}
}
class B1 : A { }
class B2 : A, IC { }
class Program
{
static void Main()
{
List<IC> cs = new List<IC>();
cs.Add(new A());
cs.Add(new B1());
cs.Add(new B2());
foreach (IC c in cs)
c.M();
}
}
程序能夠正常編譯,運行結果是:
In class A
In class A
In class A
OH, MY GOD! 怎麼效果又一樣!難道B1跟B2真的沒區別??我再把代碼修改一下:
interface IC
{
void M();
}
class A : IC
{
void IC.M()
{
Console.WriteLine("In class A");
}
}
class B1 : A
{
void IC.M()
{
Console.WriteLine("In class B1");
}
}
class B2 : A, IC
{
void IC.M()
{
Console.WriteLine("In class B2");
}
}
Oh,代碼無法編譯,編譯器發脾氣了:
'B1.IC.M()': containing type does implement interface 'IC'
換句話,我們不能再B1裡面重新實現IC.M方法,我們只能默默地接受從繼類繼承而來的那一個了!再修改一下:
interface IC
{
void M();
}
class A : IC
{
void IC.M()
{
Console.WriteLine("In class A");
}
}
class B1 : A { }
class B2 : A, IC
{
void IC.M()
{
Console.WriteLine("In class B2");
}
}
class Program
{
static void Main()
{
List<IC> cs = new List<IC>();
cs.Add(new A());
cs.Add(new B1());
cs.Add(new B2());
foreach (IC c in cs)
c.M();
}
}
這些編譯正常通過了,得到的結果是:
In class A
In class A
In class B2
3. 得出結論 & 新問題展現
好吧,有結果了,B1和B2兩種繼承方式的效果的確不同,具體體現在多態行為上(有關多態的介紹,你可以參見《今天你多態了嗎?》一文)。B1是個可憐蟲,它必須接受A對IC.M的實現,無法改變這種命運;然而B2就不同,它有權選擇接受還是拒絕,當然,拒絕的條件是提供有自己特色的實現。
4. 探索新問題 & 解決新問題
那麼,我們如何糾正這種非預期的多態行為呢?一個簡單的回答就是把B1的聲明改成跟B2的一樣。但這樣,所有繼承於A的派生類都必須照做,沒得商量!還有其他的辦法嗎?有的,請先看如下代碼:
interface IC
{
void M();
}
class A : IC
{
void IC.M()
{
this.M();
}
public virtual void M()
{
Console.WriteLine("In class A");
}
}
class B1 : A
{
public override void M()
{
Console.WriteLine("In class B1");
}
}
class B2 : A, IC
{
public override void M()
{
Console.WriteLine("In class B2");
}
}
class Program
{
static void Main()
{
List<IC> cs = new List<IC>();
cs.Add(new A());
cs.Add(new B1());
cs.Add(new B2());
foreach (IC c in cs)
c.M();
}
}
運行結果為:
In class A
In class B1
In class B2
這樣,多態的效果就如我們所願了!當然,現在B2聲明中的IC又顯得有點多余了,但你可以輕松把它拿掉!另外,如果測試程序換成:
class Program
{
static void Main()
{
List<A> ace = new List<A>();
ace.Add(new A());
ace.Add(new B1());
ace.Add(new B2());
foreach (A a in ace)
a.M();
}
}
結果還是一樣!
5. 是的,我說謊了。[New]
或許你已經注意到,在上面的整個過程中,我做了一個最大的假設,那就是我可以任我喜歡修改A的源代碼!也因為這樣,我可以輕松的糾正這些非預期的多態行為。但實際的情況是,我們不會每次都那麼幸運。如果我們僅僅得到一個包含類A和接口IC的程序集呢?那麼,我們就需要使用到接口的重新映射了。實際上,B2就是使用這種技巧。還是讓我們來看看具體的情況:
接口IC的規格不變。
我們只知道類A的聲明以及它的成員列表和對應的輸出:
Class class A : IC Output
Method public void M(); In class A
Method void IC.M(); In class A
現在我需要實現一批繼承於A的派生類,但我不希望同時繼承A的對應方法的實現,我該怎麼做?很簡單,首先創建一個類AX繼承自類A和接口IC,並在AX裡面處理好相關的事宜,接著讓那批派生類繼承於AX:
class AX : A, IC
{
// 這裡使用new是聲明其與基類的同名方法M沒有任何瓜葛。
// 使用virtual是為後代的繼承打下鋪墊。
public new virtual void M()
{
Console.WriteLine("In class AX");
}
void IC.M()
{
this.M();
}
}
class B1 : AX
{
public override void M()
{
Console.WriteLine("In class B1");
}
}
class B2 : AX
{
public override void M()
{
Console.WriteLine("In class B2");
}
}
好吧,然我們來看看測試程序:
class Program
{
static void Main(string[] args)
{
List<IC> cs = new List<IC>();
cs.Add(new A());
cs.Add(new AX());
cs.Add(new B1());
cs.Add(new B2());
foreach (IC c in cs)
c.M();
Console.WriteLine();
List<AX> ace = new List<AX>();
ace.Add(new AX());
ace.Add(new B1());
ace.Add(new B2());
foreach (AX a in ace)
a.M();
Console.ReadLine();
}
}
我想你已經猜到運行結果了:
In class A
In class AX
In class B1
In class B2
In class AX
In class B1
In class B2
好吧,你辛苦了,如果還沒有頭暈的話,請再聽我說一句。接口重新映射究竟是一個問題還是一種技巧,那要看你實際遭遇的情況。如果你能夠靈活運用的話,它的確會為你帶來巨大的便利!
6. 繼承問題的一些易混淆的地方
請留意下面的代碼:
interface IC1 { }
interface IC2 : IC1 { }
class A1 : IC1 { }
class A2 : IC1, IC2 { }
class B1 : A1 { }
class B2 : A1, IC1 { }
其中,A1和A2是沒有實質的區別的,詳細請看《接口繼承的聲明問題》一文;而B1和B2卻在某些場合表現出不同的行為,為何B1和B2會有這種差異,相信現在的你應該有所了解了吧!
7. IL呢?[Updated]
噢,對了,Ninputer的問題還有個“編譯器在IL級別是用什麼手段實現這個功能的呢?”!如果你看完本文後還嫌不夠,希望更加深入了解一下IL層次上,CLR是怎樣實現接口重新映射的原理的話,我推薦你閱讀《接口映射的實現及原理》。