CLR via C#, 4th -- 【设计类型】 -- 第13章接口
13.1 类和接口继承
Microsoft NET Framework提供了System.Object类,它定义了4个公共实例方法:ToString,Equals,GetHashCode和GetType.该类是其他所有类的根或者说终极基类。
由于Microsoft的开发团队已实现了Object的方法,所以从Object派生的任何类实际都继承了以下内容。
方法签名
使代码认为自己是在操作Object类的实例,但实际操作的可能是其他类的实例。
方法实现
使开发人员定义Object的派生类时不必手动实现Object的方法。
CLR还允许开发人员定义接口,它实际只是对一组方法签名进行了统一命名。这些方法不提供任何实现。类通过指定接口名称来继承接口,而且必须显式实现接口方法,否则CLR会认为此类型定义无效。
类继承的一个重要特点是,凡是能使用基类型实例的地方,都能使用派生类型的实例。类似地,接口继承的一个重点特点是,凡是能使用具名接口类型的实例的地方,都能使用实现了接口的一个类型的实例。
13.2 定义接口
接口不能定义任何构造器方法,也不能定义任何实例字段。
虽然CLR允许接口定义静态方法、静态字段、常量和静态构造器,事实上,C#禁止接口定义任何一种这样的静态成员。
public interface IDisposable { void Dispose(); } public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerable<out 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; } // Read only property Boolean IsReadOnly { get; } // Read only property }
根据约定,接口类型名称以大写字母I开头,目的是方便在源代码中辨认接口类型。
接口定义可从另一个或多个接口“继承”。但“继承”应打上引号,因为它并不是严格的继承。接口继承的工作方式并不完全和类继承一样。我个人倾向于将接口继承看成是将其他接口的协定(contract)包括到新接口中。
例如,ICollection<T>接口定义就包含了IEnumerable<T>和IEnumerable两个接口的协定。这有下面两层含义。
- 继承ICollection<T>接口的任何类必须实现ICollection<T>,IEnumerable<T>和IEnumerable这三个接口所定义的方法。
- 任何代码在引用对象时,如果期待该对象的类型实现了ICollection<D>接口,可以认为该类型还实现了IEnumerable<T>和IEnumerable接口。
13.3 继承接口
C#编译器要求将实现接口的方法(后文简称为“接口方法”)标记为public,CLR要求将接口方法标记为virtual。不将方法显式标记为virtual,编译器会将它们标记为virtual和sealed;这会阻止派生类重写接口方法。将方法显式标记为virtual,编译器就会将该方法标记为virtual(并保持它的非密封状态),使派生类能重写它。
派生类不能重写sealed的接口方法。但派生类可重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是该方法在该对象的类型中的实现。
using System; public static class Program { public static void Main() { /************************* First Example *************************/ Base b = new Base(); // Calls Dispose by using b's type: "Base's Dispose" b.Dispose(); // Calls Dispose by using b's object's type: "Base's Dispose" ((IDisposable)b).Dispose(); /************************* Second Example ************************/ Derived d = new Derived(); // Calls Dispose by using d's type: "Derived's Dispose" d.Dispose(); // Calls Dispose by using d's object's type: "Derived's Dispose" ((IDisposable)d).Dispose(); /************************* Third Example *************************/ b = new Derived(); // Calls Dispose by using b's type: "Base's Dispose" b.Dispose(); // Calls Dispose by using b's object's type: "Derived's Dispose" ((IDisposable)b).Dispose(); } } // This class is derived from Object and it implements IDisposable internal class Base : IDisposable { // This method is implicitly sealed and cannot be overridden public void Dispose() { Console.WriteLine("Base's Dispose"); } } // This class is derived from Base and it reimplements IDisposable internal class Derived : Base, IDisposable { // This method cannot override Base's Dispose. 'new' is used to indicate // that this method reimplements IDisposable's Dispose method new public void Dispose() { Console.WriteLine("Derived's Dispose"); // NOTE: The next line shows how to call a base class's implementation (if desired) // base.Dispose(); } }
13.4 关于调用接口方法的更多探讨
CLR允许定义接口类型的字段、参数或局部变量。使用接口类型的变量可以调用该接口定义的方法。
// The s variable refers to a String object. String s = "Jeffrey"; // Using s, I can call any method defined in // String, Object, IComparable, ICloneable, IConvertible, IEnumerable, etc. // The cloneable variable refers to the same String object ICloneable cloneable = s; // Using cloneable, I can call any method declared by the // ICloneable interface (or any method defined by Object) only. // The comparable variable refers to the same String object IComparable comparable = s; // Using comparable, I can call any method declared by the // IComparable interface (or any method defined by Object) only. // The enumerable variable refers to the same String object // At run time, you can cast a variable from one interface to another as // long as the object's type implements both interfaces. IEnumerable enumerable = (IEnumerable) comparable; // Using enumerable, I can call any method declared by the // IEnumerable interface (or any method defined by Object) only.
13.5 隐式和显式接口方法实现(幕后发生的事情)
internal sealed class SimpleType : IDisposable { public void Dispose() { Console.WriteLine("Dispose"); } }
类型的方法表将包含以下方法的记录项。
- Object(隐式继承的基类)定义的所有虚实例方法。
- IDisposable(继承的接口)定义的所有接口方法。本例只有一个方法,即Dispose,因为IDisposable接口只定义了这个方法。
- SimpleType引入的新方法Dispose.
为简化编程,C#编译器假定SimpleType引入的Dispose方法是对IDisposable的Dispose方法的实现。之所以这样假定,是由于Dispose方法的可访问性是public,而接口方法的签名和新引入的方法完全一致。也就是说,两个方法具有相同的参数和返回类型。顺便说一句,如果新的Dispose方法被标记为virtual,C#编译器仍然认为该方法匹配接口方法。
C#编译器将新方法和接口方法匹配起来之后,会生成元数据,指明SimpleType类型的方法表中的两个记录项应引用同一个实现。为了更清楚地理解这一点,下面的代码演示了如何调用类的公共Dispose方法以及如何调用IDisposable的Dispose方法在类中的实现:
public sealed class Program { public static void Main() { SimpleType st = new SimpleType(); // This calls the public Dispose method implementation st.Dispose(); // This calls IDisposable's Dispose method implementation IDisposable d = st; d.Dispose(); } }
internal sealed class SimpleType : IDisposable { public void Dispose() { Console.WriteLine("public Dispose"); } void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); } }
在C#中,将定义方法的那个接口的名称作为方法名前缀(例如IDisposable.Dispose),就会创建显式接口方法实现(Explicit Interface Method Implementation,EIMI)。注意,C#中不允许在定义显式接口方法时指定可访问性(比如public或private),但是,编译器生成方法的元数据时,可访问性会自动设为private,防止其他代码在使用类的实例时直接调用接口方法。只有通过接口类型的变量才能调用接口方法。
EIMI方法不能标记为virtual,所以不能被重写。这是由于EIMI方法并非真的是类型的对象模型的一部分,它只是将接口(一组行为或方法)和类型连接起来,同时避免公开行为/方法。
13.6 泛型接口
首先,泛型接口提供了出色的编译时类型安全性。
private void SomeMethod1() { Int32 x = 1, y = 2; IComparable c = x; // CompareTo expects an Object; passing y (an Int32) is OK c.CompareTo(y); // y is boxed here // CompareTo expects an Object; passing "2" (a String) compiles // but an ArgumentException is thrown at runtime c.CompareTo("2"); }
接口方法理想情况下应该使用强类型。
private void SomeMethod2() { Int32 x = 1, y = 2; IComparable<Int32> c = x; // CompareTo expects an Int32; passing y (an Int32) is OK c.CompareTo(y); // y is not boxed here // CompareTo expects an Int32; passing "2" (a String) results // in a compiler error indicating that String cannot be cast to an Int32 c.CompareTo("2"); // Error }
泛型接口的第二个好处在于,处理值类型时装箱次数会少很多。
泛型接口的第三个好处在于,类可以实现同一个接口若干次,只要每次使用不同的类型参数。
using System; // This class implements the generic IComparable<T> interface twice public sealed class Number: IComparable<Int32>, IComparable<String> { private Int32 m_val = 5; // This method implements IComparable<Int32>'s CompareTo public Int32 CompareTo(Int32 n) { return m_val.CompareTo(n); } // This method implements IComparable<String>'s CompareTo public Int32 CompareTo(String s) { return m_val.CompareTo(Int32.Parse(s)); } } public static class Program { public static void Main() { Number n = new Number(); // Here, I compare the value in n with an Int32 (5) IComparable<Int32> cInt32 = n; Int32 result = cInt32.CompareTo(5); // Here, I compare the value in n with a String ("5") IComparable<String> cString = n; result = cString.CompareTo("5"); } }
接口的泛型类型参数可标记为逆变和协变,为泛型接口的使用提供更大的灵活性。
13.7 泛型和接口约束
第一个好处在于,可将泛型类型参数约束为多个接口。
public static class SomeType { private static void Test() { Int32 x = 5; Guid g = new Guid(); // This call to M compiles fine because // Int32 implements IComparable AND IConvertible M(x); // This call to M causes a compiler error because // Guid implements IComparable but it does not implement IConvertible M(g); } // M's type parameter, T, is constrained to work only with types that // implement both the IComparable AND IConvertible interfaces private static Int32 M<T>(T t) where T : IComparable, IConvertible { ... } }
事实上,如果将T约束为一个类和两个接口,就表示传递的实参类型必须是指定的基类(或者它的派生类),而且必须实现两个接口。
接口约束的第二个好处是传递值类型的实例时减少装箱。
13.8实现多个具有相同方法名和签名的接口
定义实现多个接口的类型时,这些接口可能定义了具有相同名称和签名的方法。
public interface IWindow { Object GetMenu(); } public interface IRestaurant { Object GetMenu(); }
要定义实现具有相同名称和签名方法的类型,必须使用“显式接口方法实现”来实现这个类型的成员,
// This type is derived from System.Object and // implements the IWindow and IRestaurant interfaces. public sealed class MarioPizzeria : IWindow, IRestaurant { // This is the implementation for IWindow's GetMenu method. Object IWindow.GetMenu() { ... } // This is the implementation for IRestaurant's GetMenu method. Object IRestaurant.GetMenu() { ... } // This (optional method) is a GetMenu method that has nothing // to do with an interface. public Object GetMenu() { ... } }
代码在使用MarioPizeria对象时必须将其转换为具体的接口才能调用所需的方法
MarioPizzeria mp = new MarioPizzeria(); // This line calls MarioPizzeria's public GetMenu method mp.GetMenu(); // These lines call MarioPizzeria's IWindow.GetMenu method IWindow window = mp; window.GetMenu(); // These lines call MarioPizzeria's IRestaurant.GetMenu method IRestaurant restaurant = mp; restaurant.GetMenu();
13.9用显式接口方法实现来增强编译时类型安全性
public interface IComparable { Int32 CompareTo(Object other); }
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 Main() { SomeValueType v = new SomeValueType(0); Object o = new Object(); Int32 n = v.CompareTo(v); // Undesired boxing n = v.CompareTo(o); // InvalidCastException }
上述代码存在两个问题。
不希望的装箱操作
v作为实参传给CompareTo方法时必须装箱,因为CompareTo期待的是一个Object
缺乏类型安全性
代码能通过编译,但CompareTo方法内部试图将。转换为SomeValueType时抛出InvalidCastException异常。
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); } // NOTE: No public/private used on the next line Int32 IComparable.CompareTo(Object other) { return CompareTo((SomeValueType) other); } }
public static void Main() { SomeValueType v = new SomeValueType(0); Object o = new Object(); Int32 n = v.CompareTo(v); // No boxing n = v.CompareTo(o); // compiletime error }
不过,定义接口类型的变量会再次失去编译时的类型安全性,而且会再次发生装箱:
public static void Main() { SomeValueType v = new SomeValueType(0); IComparable c = v; // Boxing! Object o = new Object(); Int32 n = c.CompareTo(v); // Undesired boxing n = c.CompareTo(o); // InvalidCastException }
13.10 谨慎使用显式接口方法实现
EIMI最主要的问题如下
- 没有文档解释类型具体如何实现一个EIMI方法,也没有Microsoft Visual Studio“智能感知”支持。
- 值类型的实例在转换成接口时装箱。
- EIMI不能由派生类型调用。
下面的代码无法编译:
public static void Main() { Int32 x = 5; Single s = x.ToSingle(null); // Trying to call an IConvertible method }
要在一个Int32上调用ToSingle,首先必须将其转换为1Convertible,如下所示:
public static void Main() { Int32 x = 5; Single s = ((IConvertible) x).ToSingle(null); }
对类型转换的要求不明确,而许多开发人员自己看不出来问题出在哪里。还有一个更让人烦恼的问题:Int32值类型转换为IConvertible会发生装箱,既浪费内存,又损害性能。这是本节开头提到的EIMI存在的第二个问题。
EIMI的第三个也可能是最大的问题是,它们不能被派生类调用。下面是一个例子:
internal class Base : IComparable { // Explicit Interface Method Implementation Int32 IComparable.CompareTo(Object o) { Console.WriteLine("Base's CompareTo"); return 0; } } internal sealed class Derived : Base, IComparable { // A public method that is also the interface implementation public Int32 CompareTo(Object o) { Console.WriteLine("Derived's CompareTo"); // This attempt to call the base class's EIMI causes a compiler error: // error CS0117: 'Base' does not contain a definition for 'CompareTo' base.CompareTo(o); return 0; } }
// A public method that is also the interface implementation public Int32 CompareTo(Object o) { Console.WriteLine("Derived's CompareTo"); // This attempt to call the base class's EIMI causes infinite recursion IComparable c = this; c.CompareTo(o); return 0; }
这个版本将this转换成IComparable变量c,然后用c调用CompareTo。但Derived的公共CompareTo方法充当了Derived的IComparable.CompareTo方法的实现,所以造成了无穷递归。这可以通过声明没有IComparable接口的Derived类来解决:
internal sealed class Derived : Base /*, IComparable */ { ... }
有时不能因为想在派生类中实现接口方法就将接口从类型中删除。解决这个问题的最佳方法是在基类中除了提供一个被选为显式实现的接口方法,还要提供一个虚方法。
internal class Base : IComparable { // Explicit Interface Method Implementation Int32 IComparable.CompareTo(Object o) { Console.WriteLine("Base's IComparable CompareTo"); return CompareTo(o); // This now calls the virtual method } // Virtual method for derived classes (this method could have any name) public virtual Int32 CompareTo(Object o) { Console.WriteLine("Base's virtual CompareTo"); return 0; } } internal sealed class Derived : Base, IComparable { // A public method that is also the interface implementation public override Int32 CompareTo(Object o) { Console.WriteLine("Derived's CompareTo"); // Now, we can call Base's virtual method return base.CompareTo(o); } }
13.11 设计:基类还是接口
IS-A对比CAN-DO关系
类型只能继承一个实现。如果派生类型和基类型建立不起IS-A关系,就不用基类而用接口。接口意味着CAN-DO关系。如果多种对象类型都“能”做某事,就为它们创建接口。例如,一个类型能将自己的实例转换为另一个类型(IConvertible),一个类型能序列化自己的实例(ISerializable),注意,值类型必须从System.ValueType派生,所以不能从一个任意的基类派生。这时必须使用CAN-DO关系并定义接口。
易用性
对于开发人员,定义从基类派生的新类型通常比实现接口的所有方法容易得多。基类型可提供大量功能,所以派生类型可能只需稍做改动。而提供接口的话,新类型必须实现所有成员。
一致性实现
无论接口协定(contract)订立得有多好,都无法保证所有人百分之百正确实现它。
版本控制
向基类型添加一个方法,派生类型将继承新方法。一开始使用的就是一个能正常工作的类型,用户的源代码甚至不需要重新编译。而向接口添加新成员,会强迫接口的继承者更改其源代码并重新编译。
最后要说的是,两件事情实际能同时做:定义接口,同的提供实现该接口的基类。
浙公网安备 33010602011771号