• 重温CLR(九) 接口


           对于多继承(multiple inheritance)的概念,许多程序员并不陌生,他是指一个类从两个或多个基类派生的能力。例如,假定TransmitData类的作用是发送数据,ReceiveData类的作用是接收数据。现在要创建SocketPort类,作用是发送和接收数据。在这种情况下,你会希望SocketPort从TransmitData和ReceiveData这两个类继承。

           有的编程语言允许多继承,所以能从transmitData和receiveData这两个基类派生出SocketPort。但clr不支持多继承(因此所有托管编程语言也支持不了)。clr只是通过接口提供了“缩水版”的多继承,就是接口。本章我们讨论如何定义和使用接口,还提供一些指导性原则,以便你判断何时应该使用接口而不是基类。

    类和接口继承

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

           由于Microsoft的开发团队已实现了object的方法,所以从object派生的任何类实际都继承了一下内容。

    方法签名

           使代码认为自己是在操作object类的实例,但实际操作的可能是其他类的实例。

    方法实现

           是开发人员定义object的派生类时不必手动实现object的方法

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

    CLR允许开发人员定义接口,它实际只是对一组方法签名进行了统一的命名。这些方法没有提供任何实现。类通过指定接口与名称来实现这个接口,而且必须显式实现接口方法,否则CLR会认为此类型定义无效。当然,实现接口方法的过程可能比较繁琐,所以我才在前面说接口继承是实现多继承的一种缩水版机制。c#编译器和clr允许一个类继承多个接口。当然,继承的所有接口方法都必须实现。

    类继承有一个重要特点,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似的,凡是能使用具有接口类型实例的地方,都能使用实现了这个接口的一个类型的实例。

    定义接口

           接口对一组方法签名进行了统一命名。注意,接口还能定义事件、无参属性和有参属性(c#的索引器)。所有这些东西本质上都是方法,他们只是语法上的简化。不过,接口不能定义任何构造器方法,也不能定义任何实例字段。

           虽然clr允许接口定义静态方法、静态字段、常量和静态构造器,但符合cls标准的接口决不允许,因为有的编程语言不能定义或访问它们。事实上,c#禁止接口定义任何一种这样的静态成员。

           C#用interface关键字定义接口。要为接口指定名称和一组实例方法签名。下面是fcl中的几个接口定义:

    public interface IDisposable {
        void Dispose();
    }
    public interface IEnumerable {
        IEnumerator GetEnumerator();
    }
    public interface IEnumerable<T> : IEnumerable {
        new IEnumerator<T> GetEnumerator();
    }
    public interface ICollection<T>: IEnumerable<T>, IEnumerable
    {
    void Add(T item);
    void Clear();
    Boolean Contains(T item);
    void CopyTo(T[] array,Int32 arrayIndex)
    Boolean Remove(T item);
    Int32 Count{get;}//只读属性
    Boolean IsReadOnly{get;}//只读属性
    }

           在clr看来,接口定义就是类型定义。也就是说,clr会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。和类型一样,接口可在文件范围中定义,也可嵌套在另一个类型中。定义接口类型时,可指定你希望的任何可见性/可访问性(public,protected,internal等)

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

           接口定义可从另一个或多个接口“继承”。但“继承”应打上引号,因为它并不是严格的继承。接口继承的工作方式并不全程和类继承一样,我个人倾向于将接口继承看成是将其他接口协定(contract)包括到新接口中。例如ICollection<T>:接口定义就包含了IEnumerable<T>和IEnumerable两个接口的协定。这有下面两层含义

    1 继承ICollection<T>接口的任何类必须实现ICollection<T>,IEnumerable<T>和IEnumerable这三个接口所定义的方法

    2 任何代码在引用对象时,如果期待该对象的类实现了ICollection<T>接口,可以认为该类型还实现了IEnumerable<T>和IEnumerable接口。

    继承接口

           本节介绍如何定义实现了接口的类型,然后介绍如何创建该类型的实例,并用这个对象调用接口方法。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();
            // 用b的对象的类型来调用Dispose,结果显示: "Base's Dispose"
            ((IDisposable)b).Dispose();
            /************************* 第二个例子 ************************/
            Derived d = new Derived();
            // 结果显示: "Derived's Dispose"
            d.Dispose();
            // 用d的对象的类型来调用Dispose,结果显示: "Derived's Dispose"
            ((IDisposable)d).Dispose();
            /************************* 第三个例子 *************************/
            b = new Derived();
            // 用b的类型来调用Dispose,结果显示: "Base's Dispose"
            b.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;

           在这段代码中,所有变量都引用同一个“jeffrey”string对象。该对象在托管堆中;所以,使用其中任何变量时,调用的任何方法都会影响“feffrey”string对象。不过,变量的类型规定了能对这个对象执行的操作。s变量是string类型,所以可以用s调用string类型定义的任何成员。还可用变量s调用从object继承的任何方法。

    和引用类型相似,值类型也可以实现零个或多个接口。不过,将值类型的实例转型为接口类型时必须装箱。这是由于接口变量是一个引用,它必须指向堆上的一个对象,使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方法在类中的实现:

    public static void Main()
    {
        SimpleType st = new SimpleType();
    
        // 调用公共的 Dispose 方法实现
        st.Dispose();
    
        // 调用 IDisposable 的 Dispose 方法实现
        IDisposable d = st;
        d.Dispose();
    }

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

    在第一个dispose方法调用中,调用的是SimpleType定义的dispose方法。然后定义IDisposable接口类型的变量d,它引用SimpleType对象。调用SimpleType时,调用的是IDisposable接口的dispose方法。由于c#要求公共dispose方法同时是IDisposable的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),就会创建显式接口方法实现(explicit interface Method Implementation,EIMI)。注意,c#中不允许在定义显式接口方法时指定可访问性(比如public或private)。但是,编译器生成方法的元数据时,可访问性会自动设为private,防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口方法。

           还要还要好主意,EIMI方法不能标记为virtual,所以不能被重写。这是由于eimi方法并非真的是类型的对象模型的一部分,它只是将接口和类型连接起来,同时避免公开行为/方法。

    泛型接口

           C#和CLR所支持的泛型接口为开发人员提供了许多非常出色的功能。本节要讨论泛型接口提供的一些一些好处。

           首先,泛型接口提供了出色的编译时类型安全性。有的接口(比如非泛型IComparable接口)定义的方法使用了Object参数或Object返回类型。在代码中调用这些接口方法时,可传递对任何类型的实例的引用。但这通常不是我们期望的。下面的代码对此进行了演示:

    private static void SomeMethod1()
    {
        Int32 x = 1, y = 2;
        IComparable c = x;
    
        // CompareTo 期望接口一个 Object 类型; 传递 y (一个 Int32 类型) 允许
        c.CompareTo(y);     // y在这里装箱
    
        // 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);     // y在这里不装箱
    
        // CompareTo 期望接口一个 Int32 类型; 传递 "2" (一个 String 类型) 编译不通过
        // 指出 String 不能被隐式转型为 Int32
        // c.CompareTo("2");
    }

           泛型接口的第二个好处在于,处理值类型时装箱次数会少很多。

    注意:fcl定义了IComparable、ICollection等接口的泛型和非泛型版本。定义类型时要实现其中任何接口,一般应使用泛型版本。fcl保留非泛型版本是为了向后兼容。

           泛型接口的第三个好处在于,类可以实现同一个接口若干次,只要每次使用不同的类型参数。

    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));
        }
    }

           接口的泛型类型参数可标记为逆变和协变,为泛型接口的使用提供更大的灵活性。

    泛型和接口约束

           上一节讨论了泛型接口的好处。本节要讨论泛型类型参数约束为接口的好处。

           第一个好处在于,可将泛型类型参数约束为多个接口。这样一来,传递的参数的类型必须实现全部接口约束。

    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;
        }
    }

           这真的很酷!定义方法参数时,参数的类型规定了传递的实参必须是该类型或者它的派生类型。如果参数的类型是接口,那么实参可以是任意类类型,只要该类实现了接口。使用多个接口约束,实际是标识向方法传递的实参必须实现多个接口。

           事实上,如果将T约束为一个类和两个接口,就标识传递的实参类型必须是指定的基类(或者它的派生类),而且必须实现两个接口。这种灵活性使方法能细致地约束调用者能传递的内容。调用者不满足这些约束,就会产生编译错误。

           接口约束的第二个好处是传递值类型的实例时减少装箱。上述代码向M方法传递了x(值类型int实例)。x传给M方法时不会发生装箱。

           c#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口方法而不装箱。不用接口约束边没有其他办法让c#编译器生成这些IL指令。一个例外是如果值类型实现了一个接口方法,在值类型的实例上调用这个方法不会造成值类型的实例装箱。

    实现多个具有相同方法名和签名的接口

    定义实现多个接口的一个类型时,这些接口可能定义了具有相同名称和签名的方法。例如

    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;
        }
    }

      由于这个类型必须实现多个接口的GetMenu方法,所以要告诉c#编译器每个GetMenu方法对应的是哪个接口的实现。

      代码在使用一个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预期接受类型是Objec,它必须进行装箱。

    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);
         }
         // 注意: 这个是显示实现接口  没有指定public或者private的可访问性
         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);           //编译时错误
    }

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

    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也可能造成一些严重后果,所以应该尽量避免使用EIMI。幸好,泛型接口可帮助我们在大多数时候避免使用EIMI。但有时,比如实现具有相同名称和签名的两个接口方法时,仍需要它们。

    EIMI最主要的问题如下:

    1)没有文档解释一个类型具体如何实现一个EIMI方法,也没有vs的智能感知。

    2)值类型的实例在转型为接口时装箱

    3)EIMI不能由派生类调用

           下面详细讨论下这些问题。

           例如,int32类型的文档只是说它实现了IConvertible接口的所有方法。但你不能直接在一个int32上调用IConvertible方法。例如下面代码无法编译

    public static void Main()
    {
         int x=5;
         Single s=x.ToSingle(null);
    }

    编译这个方法时,c#编译器会提示 error cs0117 int不包含ToSingle的定义。这个错误信息使开发人员感到困惑,因为它实际上是定义了。

           要在一个int32上调用toSingle,首先必须将其转换为IConvertible接口,然后再进行调用,如下

    public static void Main()
    {
         int x=5;
         Single s=((IConvertible) x).ToSingle(null);
    }

           对类型转换的要求不明确,让许多开发人员看不出自己的问题在哪,还有一个更让人烦恼的问题,int值类型转换为IConvertible会发生装箱,既浪费内存,又损害性能。

           EIMI的第三个,也是最大的问题,他们不能被派生类调用。

    设计 :基类还是接口

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

    1) IS-A vs. CAN-DO关系

    类型只能继承一个实现。如果派生类型和基类型不能建立起is –a关系,就不用基类而用接口。接口意味着Can do关系。如果多种对象类型都能做某事,就为它们创建接口。例如,一个类型能将自己的实例转换转换二维另一个类型(IConvertible),一个类型能序列化自己的实例(ISerializable)。注意,值类型必须从system.valueType派生,所以不能从一个人一的基类派生。这时必须使用can do关系并定义接口。

    2) 易用性

    对于开发人员,定义从基类派生的新类型通常比实现接口的所有方法容易得多。基类可提供大量功能,所以派生类型可能只需要稍微改动。而提供接口的话,新类型必须实现所有成员

    3) 一致性的实现

    无论接口协定订立得有多好,都无法保证所有人百分之百正确实现它。事实上,com颇受该问题之累。而如果为基类型提供良好的默认实现,那么一开始得到的就是能正常工作并经过良好测试的类型。以后根据需要修改就可以了。

    4) 版本控制

    向基类型添加一个方法,派生类将继承新方法。一开始使用就是一个能正常工作的类型,用户的源代码甚至不需要编译。向接口添加一个新成员,会强迫接口的实现者更改其源代码

    最后要指出的是,这两件事情实际上是可以同时做:定义一个接口,同时提供一个实现了这个接口的基类。

           FCL中涉及数据流处理的类采用的是实现继承方法。system.IO.Stream是抽象基类,提供了包括read和write在内的一组方法。其他类(filestream,memoryStream和NetWorkStream等)都从stream派生。在这三个类中,每一个和stream类都是is –a关系,这使得具体类的实现变得更容易。

           相反,Microsoft采用基于接口的方式设计FCL中的集合。

  • 相关阅读:
    242. Valid Anagram
    [wikioi]关押罪犯
    [wikioi]数的划分
    [wikioi]能量项链
    [wikioi]线段覆盖 2
    [wikioi]乌龟棋
    POJ1011 Sticks
    *[leetcode]Word Break II
    [leetcode]Word Break
    《高性能网站建设指南》笔记
  • 原文地址:https://www.cnblogs.com/qixinbo/p/10593851.html
Copyright © 2020-2023  润新知