首页 > 学院 > 开发设计 > 正文

[CLR via C#]13. 接口

2019-11-17 03:20:44
字体:
来源:转载
供稿:网友

[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 DisposeIDisposable 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.类可以实现
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表