一、类和接口继承
在Microsoft.Net Framwork中,有一个名为System.Object的类,它定义了4个公共实例方法:ToString, Equals, GetHashCode和GetType。该类是其他所有类的根或者说最终基类。换言之,所有类都继承了Object的4个实例方法。这还意味着能操作Object类的实例的代码实际能操作任何类的实例。
在CLR中,任何类都肯定从一个类也只能从一个类(而且只能从Objetc派生的类)派生的。这个类称为基类。基类提供了一组方法签名和这些方法的实现。你定义的新类可在将来由其它开发人员用作基类——所有方法签名和方法实现都会由新的派生类继承。
二、定义接口
接口对一组方法签名进行了统一命名。接口还能定义事件,无参属性和有参属性(C#中的索引器),因为这些东西本质都是方法。接口不能定义任何构造器方法,和任何实例字段。
public interface IDisposable { void Dispose(); } public interface IEnumerable { IEnumerator GetEnumerator(); }
对CLR而言,定义接口就像定义类型,也就是说,CLR会为接口类型对象定义一个内部数据结构,同时可用反射机制来查询接口类型的功能。定义接口类型时,可指定你希望的任何访问性修饰符。
三、继承接口
本节将讲介绍如何定义一个实现了接口的类型,然后再介绍如何创建这个类型的一个实例,并用这个对象调用接口的方法。最后介绍C#接口实现时,幕后发生的事情。
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); }
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(); } } }
// 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;
interter sealed class SimpleType : IDisposable { public void Dispose() { console.WriteLine("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
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,防止其他代码在使用类的实例时直接调用接口方法。要调用接口方法,只能通过接口类型的一个变量来进行。
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"); }
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; } }
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; } } }
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(); }
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); } }
public static void Go() { SomeValueType v = new SomeValueType(0); Object o = new Object(); Int32 n = v.CompareTo(v); // 非预期装箱 n = v.CompareTo(o); // 抛出异常 }
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为这些接口的方法创建类型安全的版本,并减少值类型的装箱。
十一、设计:基类还是接口
选择基类还是接口的指导性原则: