C#筆記 – 接口

定義
  • 接口可以定義大部分方法、以及事件、屬性

    • 但不能定義構造器方法和實例字段

  • 雖然CLR允許接口可以定義靜態成員,但是符合CLS標準的接口不允許

    • 因此,C#禁止接口定義任何一種靜態成員

  • 在CLR看來,接口定義就是類型定義

    • 所以,CLR會為接口類型對象定義「內部數據結構」

    • 可以通過反射機制來查詢接口類型的功能

繼承
  • C#編譯器對「實現接口的方法」的要求

    • public interface ITestInterface
      {
         void SealTest();
         void VirtualTest();        
      }

      public class TestConcreteClass : ITestInterface
      {
         public virtual void VirtualTest(){ }
         public void SealTest(){ }
      }
    • 標志為public(必須)

    • 標志為virtual(可選)

      • 如果標志為virtual

        • 實現了接口的類的派生類則可以重寫該方法

        • 元數據

          • Method #1 (06000009) 
            -------------------------------------------------------
            MethodName: VirtualTest (06000009)
            Flags     : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
            RVA       : 0x000020d1
            ImplFlags : [IL] [Managed] (00000000)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Void
            No arguments.
      • 如果不標志為virtual

        • 該方法會被編譯器標志為virtual和sealed,方法即不能被派生類所覆寫

        • 元數據

          • Method #2 (0600000a) 
            -------------------------------------------------------
            MethodName: SealTest (0600000A)
            Flags     : [Public] [Final] [Virtual] [HideBySig] [NewSlot] (000001e6)
            RVA       : 0x000020d4
            ImplFlags : [IL] [Managed] (00000000)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Void
            No arguments.
          • 標志[Final]代表了sealed

        • 派生不能重寫sealed的方法,但是派生類可以繼承同一個接口,然後為另外提供自己的實現

          • public class TestSonConcreteClass : TestConcreteClass, ITestInterface
            {
               new public void SealTest() { }
            }
          • 在對象上調用接口方法時,調用的是該方法在該對象的類型中的實現

            • 變量的類型規定了能對這個對象執行的操作

            • 假設父類接口方法輸出一個”Base Test”;子類接口方法輸出一個”Child Test”

              • public class TestConcreteClass : ITestInterface
                {
                public void SealTest(){ Console.WriteLine("Base Test"); }
                }
                public class TestSonConcreteClass : TestConcreteClass, ITestInterface
                {
                new public void SealTest() { Console.WriteLine("Child Test"); }
                }
            • 父類聲明父類實現

              • TestConcreteClass c = new TestConcreteClass();
                c.SealTest(); //Base Test
                ((ITestInterface)c).SealTest(); //Base Test
              • c的類型對象為父類,因此SealTest的實現類型也是父類,輸出也為父類實現輸出

            • 子類聲明子類實現

              • TestSonConcreteClass sc = new TestSonConcreteClass();
                sc.SealTest(); //Child Test
                ((ITestInterface)sc).SealTest(); //Child Test
              • sc的類型對象為子類,因此SealTest的實現類型也是子類,輸出也為子類實現輸出

            • 父類聲明子類實現

              • TestConcreteClass c = new TestSonConcreteClass();
                c.SealTest(); //Base Test
                ((ITestInterface)c).SealTest(); //Child Test
              • c的聲明為父類,並以子類實例化

              • 由於是以父類聲明,因此直接調用c.SealTest的話,將會調用該「父類」中的實現

              • 如果將其強轉為ITestInterface,由於實現接口方法的對象類型是子類,因此,調用的是「子類」的實現

      • 把類強轉成接口類型後,就只能調用接口定義的方法

        • 由於所有類型都繼承自Object,接口也不例外,所以接口定義的方法也包含了Object下的一系列方法,如GetString、GetHashCode等

        • 由於接口變量為引用類型,因此,值類型強轉成接口類型時必須裝箱。使CLR能檢查對象的類型對象指針,從而判斷對象的確切類型

          • 調用已裝箱值類型的接口方法時,CLR會跟隨對象的類型對象指針找到類型對象的方法表,從而調用正確的方法

隱式接口方法實現
  • 類型加載到CLR時,為該類型創建一個內部數據結構,這個內部數據結構中包含了其定義的所有方法的指針

    • 類型引入的每個方法都有對應的記錄項,包括該類型繼承的所有虛方法

      • 這些虛方法除了基類定義的方法外,還包含了接口定義的方法

        • internal sealed class SimpleType : IDisposable
          {
             public void Dispose() { Console.WriteLine("Dispose"); }
          }
        • SimpleType的方法表將會包含以下方法的記錄項

          • Object(隱式繼承的基類)定義的所有虛實例方法

          • IDisposable(繼承的接口)定義的所有接口方法

          • SimpleType自己定義的Dispose方法

        • 當接口方法簽名和新引入的方法的簽名一致(參數和返回類型一致):

          • C#編譯器假定SimpleType自己定義的Dispose方法是對IDisposable的Dispose方法的實現

顯式接口方法實現
  • 除了直接定義一個與接口方法同樣簽名的方法來實現接口方法外,還可以直接以接口名作為方法名前綴來「顯式實現接口方法」(EIMI,Explicit Interface Method Implementation)

    • internal sealed class SimpleType : IDisposable
      {        
         //SimpleType對象的Dispose方法
         public void Dispose() { Console.WriteLine("SimpleType Dispose"); }

         //顯式實現的接口方法Dispose
         void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }
      }
    • 顯式實現的接口方法不能定義訪問修飾符,且默認為Private和Sealed的,只能通過接口類型變量去調用該方法,而且不能被重寫

      • Method #2 (06000010) 
        -------------------------------------------------------
        MethodName: System.IDisposable.Dispose (06000010)
        Flags     : [Private] [Final] [Virtual] [HideBySig] [NewSlot] (000001e1)
        RVA       : 0x0000212e
        ImplFlags : [IL] [Managed] (00000000)
        CallCnvntn: [DEFAULT]
        hasThis
        ReturnType: Void
        No arguments.
      • SimpleType st = new SimpleType();
        st.Dispose(); //Output: SimpleType Dispose
        ((IDisposable)st).Dispose(); //Output: IDisposable Dispose
  • 如果實現多個接口時,這些接口之間存在一個簽名和方法名一樣的方法,實現他們時,就必須使用EIMI實現

    • 調用時也須先把對象轉換為指定的接口類型才能正確調用需要的接口方法

    • static void Main(string[] args)
      {
         Tester tt = new Tester();
        ((ITestA)tt).Test();
        ((ITestB)tt).Test();
      }

      public interface ITestA
      {
         void Test();
      }

      public interface ITestB
      {
         void Test();
      }

      public class Tester : ITestA, ITestB
      {
         void ITestA.Test() { }

         void ITestB.Test() { }        
      }
  • 但是EIMI存在較多的使用隱患,應盡可能使用泛型接口取代

    • 比如要把對象轉換成指定接口類型才能調用對應的接口方法,必須使值類型對象發生裝箱

    • 另外,EIMI不能由派生類調用

      • EIMI方法事實上不是類型的對象模型的一部分,只是將接口和類型連接起來,提供了一個用接口類型變量來調用的方法

      • public class CompareBase : IComparable
        {
           int IComparable.CompareTo(object obj)
          {
               Console.WriteLine("Base Compare");
               return 0;
          }
        }

        public class CompareChild : CompareBase, IComparable
        {
           public int CompareTo(object o)
          {
               Console.WriteLine("Child Compare");
               //base.CompareTo(o); //base不存在CompareTo方法
               return 0;
          }
        }

泛型接口
  • 好處:

    • 提供了編譯時的類型安全性

    • 減少處理值類型時的裝箱次數

      • 泛型接口可以限制方法期待的參數類型,使得方法不再需要都期待接收一個object類參數,並在傳入值類型實參時導致裝箱發生

    • 一個接口可使用不同的類型參數來被實現N次

      • public sealed class Number : IComparable<int>, IComparable<string>
        {
           int n = 5;
           
           public int CompareTo(int other)
          {
               return n.CompareTo(other);
          }
           
           public int CompareTo(string other)
          {
               return n.CompareTo(int.Parse(other));
          }
        }
      • class Program
        {
           static void Main(string[] args)
          {
               Number n = new Number();
              ((IComparable<int>)n).CompareTo(10);
              ((IComparable<string>)n).CompareTo("10");
          }
        }

接口約束
  • 泛型類型參數可被約束為接口,限制傳遞的參數類型必須實現全部接口約束

    • void Test()
      {
         int x = 5; //Int Implemented IComparable and IConvertible
         Guid g = new Guid(); //Guid Doesn't Implement IComparable but not IConvertible
         M(x); //Compile Success
         //M(g); //Compile Failed
      }

      void M<T>(T t) where T : IComparable, IConvertible { }
  • 如果泛型類型參數被約束為接口,類型實參實際上可以是任意類型,只要該類型實現了約束所需的接口即可

  • 另外,接口約束也能減少傳遞值類型的實例時發生的裝箱

    • 如果方法定義為這樣,就會因為值類型需要被轉換為接口類型而發生裝箱

      • void Test()
        {
           int x = 5;
           //...
           N(x)
        }
        void N(IComparable t) { } //Need Box
    • C#編譯器為接口約束生成特殊IL指令,導致直接在值類型上調用接口方法而不裝箱

      • .method private hidebysig static void  Test() cil managed
        {
        //...
        IL_000b: ldloc.0
        IL_000c: call       void CLR_Ch13.Program::M<int32>(!!0)
        IL_0011: nop
        IL_0012: ldloc.0
        IL_0013: box       [System.Runtime]System.Int32
        IL_0018: call       void CLR_Ch13.Program::N(class [System.Runtime]System.IComparable)
        IL_001d: nop
        IL_001e: ret
        } // end of method Program::Test
      • 不用接口約束便沒有其他辦法讓C#編譯器生成這些IL指令,如此一來,在值類型上調用接口方法總是發生裝箱

      • 除非是在值類型實現了一個接口方法,在該實例上調用這個方法就不會發生裝箱

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter