程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 基類與接口混合繼承的聲明問題

基類與接口混合繼承的聲明問題

編輯:關於C#

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是怎樣實現接口重新映射的原理的話,我推薦你閱讀《接口映射的實現及原理》。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved