一、类和接口继承
在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.类可以实现
新闻热点
疑难解答