• [CLR via C#]13. 接口


    一、类和接口继承

      在Microsoft.Net Framwork中,有一个名为System.Object的类,它定义了4个公共实例方法:ToString, Equals, GetHashCode和GetType。该类是其他所有类的根或者说最终基类。换言之,所有类都继承了Object的4个实例方法。这还意味着能操作Object类的实例的代码实际能操作任何类的实例。

      在CLR中,任何类都肯定从一个类也只能从一个类(而且只能从Objetc派生的类)派生的。这个类称为基类。基类提供了一组方法签名和这些方法的实现。你定义的新类可在将来由其它开发人员用作基类——所有方法签名和方法实现都会由新的派生类继承。

      CLR允许开发人员定义接口,它实际只是对一组方法签名进行了统一的命名。这些方法没有提供任何实现。类通过指定接口与名称来实现这个接口,它必须显式提供接口方法的实现,否则CLR会认为此类型定义无效。
     
      类继承有一个重要特点,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似的,凡是能使用具有接口类型实例的地方,都能使用实现了这个接口的一个类型的实例。

    二、定义接口

      接口对一组方法签名进行了统一命名。接口还能定义事件,无参属性和有参属性(C#中的索引器),因为这些东西本质都是方法。接口不能定义任何构造器方法,和任何实例字段。

      虽然CLR允许接口定义静态方法、静态字段、常量和静态构造器。但C#禁止接口定义任何一种这样的静态成员。
      在C#中,使用interface关键字定义一个接口。要为接口指定一个名称和一组实例方法签名。比如FCL中的几个接口定义:
    public interface IDisposable {
        void Dispose();
    }
     
    public interface IEnumerable {
        IEnumerator GetEnumerator();
    }

      对CLR而言,定义接口就像定义类型,也就是说,CLR会为接口类型对象定义一个内部数据结构,同时可用反射机制来查询接口类型的功能。定义接口类型时,可指定你希望的任何访问性修饰符。

      根据约定,接口类型名称要以大写字母I开头,目的是方便在源代码中辨认接口类型。CLR支持泛型接口和接口中的泛型方法。
     

    三、继承接口

    本节将讲介绍如何定义一个实现了接口的类型,然后再介绍如何创建这个类型的一个实例,并用这个对象调用接口的方法。最后介绍C#接口实现时,幕后发生的事情。

    下面是在MSCorLib.dll中定义的System.IComparable<T>接口:
    public interface IComparable<in T> {
        Int32 CompareTo(T other);
    }

      以下代码展示类如何定义一个实现类该接口的类型,还展示了对象两个Point对象进行比较的代码:   

       // Point 从 System.Object 派生并且实现了 IComparable<T>.
        public sealed class Point : IComparable<Point>
        {
            private Int32 m_x, m_y;
     
            public Point(Int32 x, Int32 y)
            {
                m_x = x;
                m_y = y;
            }
     
            // 该方法实现了 IComparable<T>.CompareTo()
            public Int32 CompareTo(Point other)
            {
                return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)
                   - Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
            }
     
            public override String ToString()
            {
                return String.Format("({0}, {1})", m_x, m_y);
            }
        }
     
        public static void Go()
        {
            Point[] points = new Point[] {
             new Point(3, 3),
             new Point(1, 2),
          };
            //调用了由Point实现的IComparable<T>.CompareTo()方法
            if (points[0].CompareTo(points[1]) > 0)
            {
                Point tempPoint = points[0];
                points[0] = points[1];
                points[1] = tempPoint;
            }
            Console.WriteLine("Points from closest to (0, 0) to farthest:");
            foreach (Point p in points)
                Console.WriteLine(p);
        }
     
      C#编译器要求将用于实现一个接口的方法标记为public。CLR要求将接口方法标记为virtual。如果在源代码中没有显式地将方法标记为virtual,编译器会将它们标记为virtual和sealed;这会阻止派生类重写接口方法。如果显式地将方法标记为virtual,编译器就会将该方法标记为virtual(并保持它的非密封状态)。这样一来,派生类就能重写它了。
      如果一个接口方法是sealed的,派生类就不能重写它。不过,派生类可以重写继承同一个接口,并可为该接口的方法提供它中的实现。在一个对象上调用一个接口的方法时,将调用该方法在该对象的类型中的实现。
    internal static class InterfaceReimplementation
        {
            public static void Go()
            {
                /************************* 第一个例子 *************************/
                Base b = new Base();
     
                // 结果显示: "Base's Dispose"
                b.Dispose();
     
                // 结果显示: "Base's Dispose"
                ((IDisposable)b).Dispose();
     
     
                /************************* 第二个例子 ************************/
                Derived d = new Derived();
     
                // 结果显示: "Derived's Dispose"
                d.Dispose();
     
                // 结果显示: "Derived's Dispose"
                ((IDisposable)d).Dispose();
     
     
                /************************* 第三个例子 *************************/
                b = new Derived();
     
                // 结果显示: "Base's Dispose"
                b.Dispose();
     
                // 结果显示: "Derived's Dispose"
                ((IDisposable)b).Dispose();
            }
     
            // 这个类型派生自 Object 并且实现了 IDisposable
            internal class Base : IDisposable
            {
                // 这个方法是隐式密封的,不能被重写
                public void Dispose()
                {
                    Console.WriteLine("Base's Dispose");
                }
            }
     
            // 这个类继承了Base并且实现了IDisposable接口
            internal class Derived : Base, IDisposable
            {
                // 这个方法不能重写 Base's Dispose.
                // 'new' 关键字表明重新实现了IDisposable的Dispose
                new public void Dispose()
                {
                    Console.WriteLine("Derived's Dispose");
     
                    // 注意: 下一行展示了如何让调用基类的方法
                    // base.Dispose();
                }
            }
        }
     
    四、关于调用接口方法的更多探讨
     
      FCL的System.String类型继承了System.Object的方法签名及其实现。此外,String类型还实现了几个接口:IComparable,ICloneable,IEnumerable,IComparable<String>,IEnumerable<Char>等。这意味着String类型不需要实现(或重写)其Object基类型提供的方法,但必须实现所有接口中声明的方法。
     
      CLR允许定义接口类型的字段,参数或局部变量。使用接口类型的一个变量,可以调用由那个接口定义的方法。例如:
     
      // s变量引用了String对象,使用s时,可以调用String,
      // Object,IComparable等定义的方法
      private String s = "abc";
     
      // comparable变量引用指向同一个String对象,使用comparable
      // 只能调用IComparable接口中的定义任何方法(包括Objetc中的方法)
      private IComparable comparable = s;
     
      // convertible变量引用指向同一个String对象,使用convertible
      // 只能调用IConvertible接口中的定义任何方法(包括Objetc中的方法)
      private IConvertible convertible = s;
     
      // enumerable 变量引用同一个String对象
      // 在运行时,可将变量从一种接口类型转型到另一种类型,
      // 只要该对象的类型实现了这两个个接口
      // 使用enumerable,只能调用IEnumerable声明的任何方法(包括Objetc中的方法)
      private IEnumerable enumerable = (IEnumerable) convertible;
     
      和引用类型相似,值类型也可以实现零个或多个接口。不过,将值类型的实例转型为接口类型时,值类型的实例必须装箱。这是由于接口变量是一个引用,它必须指向堆上的一个对象,使CLR能检查对象的类型对象指针,从而判断对象的真实类型。然后,再调用已装箱值类型的一个接口方法时,CLR会跟随对象的类型对象指针,找到类型对象的方法表,从而调用正确的方法。
     
    五、 隐式和显式接口方法实现
      一个类型加载到CLR中时,会为该类型创建并初始化一个方法表。在这个方法表中,类型引入的每个新方法都有一条对象的记录项。另外,还要为该类型继承的所有虚方法添加记录项。继承的虚方法既有由继承层次结构中的各个基类型定义的,也有由接口类型定义的。所以,对于下面这样的一个简单类型定义:
    interter sealed class SimpleType : IDisposable {
        public void Dispose() { console.WriteLine("Dispose"); }
    }

      类型的方法表将包含以下方法对应的记录项:

      1)Object(隐式继承的基类)定义的所有虚实例方法。
      2)IDisposable(实现的接口)定义的所有接口方法。本例只有一个方法,即Dispose,因为IDisposable只定义了该方法。
      3)SimpleType 引入的新方法Dispose。
      为简化编程,C#编译器假定SimpleType引入的Dispose方法是对IDisposable的Dispose方法的实现。C#编译器之所以做出这样的假定,是因为Dispose方法的可访问性是public,而且接口方法的签名和新引入的方法完全一致,也就是说,这两个方法具有相同的参数和返回类型。还有,如果新的Dispose方法被标记为virtual,C#编译器仍会认为该方法匹配于接口方法。
     
      C#编译器将一个新方法和一个接口方法匹配起来之后,便会生成元数据,指明SimpleType类型的方法表中的两个记录项引用同一个实现。下面的代码演示了如果调用类的公共Dispose方法以及如何调用IDisposable的Dispose方法在类中的实现:
    internal static class ExplicitInterfaceMethodImpl
    {
        public static void Go()
        {
            SimpleType st = new SimpleType();
     
            // 调用公共的 Dispose 方法实现
            st.Dispose();
     
            // 调用 IDisposable 的 Dispose 方法实现
            IDisposable d = st;
            d.Dispose();
        }
     
        public sealed class SimpleType : IDisposable
        {
            public void Dispose() { Console.WriteLine("Dispose"); }
        }
    }

      执行后,两者是没有任何却别的。输出结果都是Dispose

      现在我们更改一下SimpleType,以便看出区别:
    public sealed class SimpleType : IDisposable
    {
         public void Dispose() { Console.WriteLine("public Dispose"); }
         void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }
    }
      现在再次程序运行,会得到如下结果;
    public Dispose
    IDisposable Dispose

      在C#中,将定义的那个接口的名称作为方法名的前缀(例如IDisposable.Dispose),创建的就是显式接口方法实现(EIMI)。注意,在C#中定义一个显式接口方法时,不允许指定可访问性。但是,编译器生成的元数据时,其访问性会被自动设为private,防止其他代码在使用类的实例时直接调用接口方法。要调用接口方法,只能通过接口类型的一个变量来进行。

     
      一个EIMI方法不能标记为virtual,所以它不能被重写这是因为EIMI方法并非真的是类型的对象模型的一部分,它是将一个接口(一组行为或方法)连接到一个类型上,同时避免公开方法的一种方式。
     
    六、 泛型接口
      本节将介绍使用泛型接口的好处。
      1.泛型接口提供了出色的编译时类型安全性。有的接口(比如非泛型的IComparable接口),定义的方法使用了Object参数或Object返回类型,但这通常不是我们想要的。
    private static void SomeMethod1()
        {
            Int32 x = 1, y = 2;
            IComparable c = x;
     
            // CompareTo 期望接口一个 Object 类型; 传递 y (一个 Int32 类型) 允许
            c.CompareTo(y);     // Boxing occurs here
     
            // CompareTo期望接口一个 Object 类型; 传递 "2" (一个 String 类型) 允许
            // 但运行是抛出 ArgumentException 异常
            c.CompareTo("2");
        }

      在理想情况下,接口方法应该使用强类型。这也正是FCL为什么还有包含一个泛型IComparable<in T>接口的原因。

    private static void SomeMethod2()
        {
            Int32 x = 1, y = 2;
            IComparable<Int32> c = x;
     
            // CompareTo 期望接口一个 Int32 类型; 传递 y (一个 Int32 类型) 允许
            c.CompareTo(y);     // Boxing occurs here
     
            // CompareTo 期望接口一个 Int32 类型; 传递 "2" (一个 String 类型) 编译不通过
            // 指出 String 不能被隐式转型为 Int32
            // c.CompareTo("2");
        }

     

      2.处理值类型时,装箱次数会少得多。
     
      3.类可以实现同一个接口若干次,只要每次使用不同的类型参数
    public static void Go()
        {
            Number n = new Number();
     
            // n 与 一个 Int32类型 5 作比较
            IComparable<Int32> cInt32 = n;
            Int32 result = cInt32.CompareTo(5);
     
            //  n 与一个 String类型 "5" 作比较
            IComparable<String> cString = n;
            result = cString.CompareTo("5");
        }
     
        // 该类实现了 IComparable<T> 接口两次
        public sealed class Number : IComparable<Int32>, IComparable<String>
        {
            private Int32 m_val = 5;
     
            // 该方法实现了 IComparable<Int32>’s CompareTo
            public Int32 CompareTo(Int32 n)
            {
                return m_val.CompareTo(n);
            }
     
            // 该方法实现了 IComparable<String>’s CompareTo
            public Int32 CompareTo(String s)
            {
                return m_val.CompareTo(Int32.Parse(s));
            }
        }

     

      4.接口的泛型类型参数可标记逆变和协变,从而为泛型接口的使用赋予更大的灵活性。详见上一篇"六、委托和接口的逆变和协变泛型类型实参"。
     
    七、泛型和接口约束
     
      前面讨论了泛型接口的好处,本节要讨论泛型类型参数约束为接口的好处。
     
      1.可将一个泛型类型参数约束为多个接口。在此过程中,传递的参数类型必须实现所有接口约束。
    public sealed class SomeType
        {
            private static void Test()
            {
                Int32 x = 5;
                Guid g = new Guid();
     
                // 对M的调用能通过编译,因为Int32实现了IComparable 和 IConvertible
                M(x);
     
                // 对M的调用能不通过编译,因为Guid实现了IComparable,但没实现了 IConvertible
                // M(g);
            }
     
            // M类型参数T被约束为需要支持同时实现IComparable 和 IConvertible interfaces接口的类型
            private static Int32 M<T>(T t) where T : IComparable, IConvertible
            {
                // ...
                return 0;
            }
        }
     
      2.传递值类型的实例时可减少装箱操作。
     
    八、实现多个具有相同方法名和签名的接口
     
      定义实现多个接口的一个类型时,这些接口可能定义了具有相同名称和签名的方法。例如    
    public interface IWindow
    {
         Object GetMenu();
    }
     
    public interface IRestaurant
    {
         Object GetMenu();
    }

      要定义一个实现了这两个接口的类型,必须使用"显示接口方法实现"来实现这个类型的成员,如下: 

       // 这个类型派生自 System.Object and
        // 并不实现 IWindow 和 IRestaurant 接口.
        public class MarioPizzeria : IWindow, IRestaurant
        {
            // 这是IWindow 的 GetMenu 方法.
            Object IWindow.GetMenu()
            {
                // ...
                return null;
            }
     
            // 这是 IRestaurant 的 GetMenu 方法.
            Object IRestaurant.GetMenu()
            {
                // ...
                return null;
            }
     
            // 这个GetMenu方法是可选的,与接口无关
            public Object GetMenu()
            {
                // ...
                return null;
            }
        }
    }
      代码在使用一个MarioPizzeria 对象时,必须把对象转型为具体的接口,以调用所需的方法。
    public static void Go()
        {
            MarioPizzeria mp = new MarioPizzeria();
     
            // 这行调用 MarioPizzeria 的公共 GetMenu 方法
            mp.GetMenu();
     
            // 这行调用 MarioPizzeria 的 IWindow.GetMenu 方法
            IWindow window = mp;
            window.GetMenu();
     
            // 这行调用 MarioPizzeria 的 IRestaurant.GetMenu 方法
            IRestaurant restaurant = mp;
            restaurant.GetMenu();
        }
    九、用显示接口方法实现来增强编译时类型安全性
      前面讨论过如何使用泛型接口增强编译时的类型安全性和减少装箱操作。遗憾的是,有些时候由于不存在泛型版本,仍然需要实现泛型接口。如果接口的任何一个方法接受System.Object类型的参数或返回一个System.Object类型的值,就会丧失编译时的类型安全性,装箱也会发生。所以,本节将介绍如何用"显示接口方法"(EIMI)在某种程度上改善这个局面。
      下面定义了一个寻常的接口:
    public interface IComparable {
          Int32 CompareTo(Objetc other);
     }

      这个接口定义了一个方法,该方法接受一个System.Object类型的参数。可像下面一样实现该接口的一个类型:

    internal struct SomeValueType : IComparable
    {
        private Int32 m_x;
        public SomeValueType(Int32 x) { m_x = x; }
        public Int32 CompareTo(Object other)
        {
            return (m_x - ((SomeValueType)other).m_x);
        }
    }
      可以使用SomeVakueType写下面的代码:
     public static void Go()
    {
       SomeValueType v = new SomeValueType(0);
       Object o = new Object();
       Int32 n = v.CompareTo(v);  // 非预期装箱
       n = v.CompareTo(o);        // 抛出异常
    }
      上述代码有两个地方不理想。
      1)非预期装箱操作    v作为实参传给CompareTo方法时,它必须进行装箱,因为CompareTo预期接受类型是Object。
      2)缺乏类型安全性    代码能通过编译,但在CompareTo方法试图进行转换时们可能会抛出异常。
     
      使用EIMI解决这两个问题:
    internal struct SomeValueType : IComparable
    {
         private Int32 m_x;
         public SomeValueType(Int32 x) { m_x = x; }
         public Int32 CompareTo(SomeValueType other)
         {
             return (m_x - other.m_x);
         }
         // 注意: 这个是显示实现接口
         Int32 IComparable.CompareTo(Object other)
         {
            return CompareTo((SomeValueType)other);
         }
    }

      注意新版本的几个改动。第一,他现在有两个CompareTo方法。第一个CompareTo方法不是获取一个Object作为参数,而是获取一个SomeValueType作为参数。这个参数改变后,就没必要再用代码将other类型转型为SomeValueType,所以用于转型的代码被去掉了。第二,修改第一个CompareTo方法,使它类型安全。这意味着SomeValueType现在没有实现ICompareTo接口,所以不满足IComparable接口的契约。所以,SomeValueType必须实现一个CompareTo方法来满足IComparable的契约。第二个IComparable.CompareTo方法正是出于这个目的而设计的,它是一个EIMI。

      这两个改动意味着想在获得了编译时的类型安全性,而且不会发生装箱:

    public static void Go()
    {
        SomeValueType v = new SomeValueType(0);
        Object o = new Object();
        Int32  n = v.CompareTo(v);   // 不发生装箱
        n = v.CompareTo(o);           
    }

      事实上,如本章前面所述,将一个值类型的实例转型为接口类型时,CLR必须对之类实例的实例进行装箱。因此,前面的Main方法中会发生两次装箱。

      不过,如果定义一个接口类型的变量,就会再次丧失编译时的类型安全性,而且再次发生非预期的装箱操作。  

    public static void Go()
    {
        SomeValueType v = new SomeValueType(0);
        IComparable c = v;                     //装箱
    
        Object o = new Object();
        Int32  n = v.CompareTo(v);            // 非预期装箱操作
        n = c.CompareTo(o);                    //InvalidCastException异常           
    }

      事实上,如本章前面所述,将一个值类型的实例转型为接口类型时,CLR必须对指向类型的实例进行转型。因此,前面的Go方法中会发生两次装箱。

      实现IConvertible, ICollection,IList和IDictionary等接口时,可利用EIMI为这些接口的方法创建类型安全的版本,并减少值类型的装箱。

    十、 谨慎使用显示接口方法实现
      
      EIMI最主要的问题如下:
      1)没有文档解释一个类型具体如何实现一个EIMI。
      2)值类型的实例在转型为接口时装箱
      3)EIMI不能由派生类调用

    十一、设计:基类还是接口

      选择基类还是接口的指导性原则:

      1)IS-A vs. CAN-DO关系    如果派生类类型不具有与基类IS-A关系,就不要使用基类,而应该使用接口;接口意味着CAN-DO关系,能做什么或获得了什么能力,如果多种对象类型都具有CAN-DO功能,那就使用接口。
        2)易于使用    定义一个从基类派生的新类型通常比实现一个接口所有方法容易的多。
        3)一致性的实现    不管一个接口的契约文档做的多好,都无法保证任何人都能百分之百正确的实现它。
        4)版本控制    向基类型添加一个方法,派生类将继承新方法。一开始使用就是一个能正常工作的类型,用户的源代码甚至不需要编译。向接口添加一个新成员,会强迫接口的实现者更改其源代码。
     
      最后要指出的是,这两件事情实际上是可以同时做:定义一个接口,同时提供一个实现了这个接口的基类。
     
  • 相关阅读:
    Java面试之对象拷贝
    Java面试之反射
    Java面试之多线程
    Java面试之容器
    Java面试之基础一
    Java面试之Hibernate
    Mysql进阶
    高并发,不怕不怕「限流算法第一把法器:计数器法」
    SpringBoot加载速度慢
    idea VM options参数优化
  • 原文地址:https://www.cnblogs.com/zxj159/p/3557813.html
Copyright © 2020-2023  润新知