接口
接口在以下情况下特别有用:
- 只有一个层次结构,但是只有一个派生类型的子集支持公开行为。
- 需要构建的公共行为为跨多个层次结构,而且除了System.Object以外,没有其他公共父类。
接口类型
定义:接口就是一组抽象成员的命名集合。
抽象方法是纯粹的协议,在其中没有提供默认的实现。由接口定义的某个特定成员依赖于它所模拟的确切行为。是的,接口表示某个类或结构可以选择去实现的行为。一个类(或者一个结构)可以支持任意数量的接口,因此本质上可就支持多种行为。
.NET基础类库提供了几百个预定义的接口类型,由各种类和结构实现。
对比接口类型和抽象基类
接口类型和抽象基类很相似。如果类被标记为抽象的,它可以定义许多抽象成员来为所有派生类型提供多态接口。然而,虽然类定义了一组抽象成员,它完全可以再定义许多构造函数、字段数据、非抽象成员(具有实现)等。而接口,只能包含抽象成员。
由抽象父类创建的多态接口有一个主要的限制,那就是只有派生类型才支持由抽象父类定义的成员。然而,在大型软件系统中,开发除了System.Object之外没有公共父类的多个类层次结构很普遍。由于抽象基类中的抽象成员只应用到派生类型,我们就不能以多个层次结构配置类型来支持相同的多态接口。
C#不支持类的多重继承。接口类型就是来解决这个问题的。在定义接口之后,它就可以被任何层次结构、任何命名空间或任何程序集(由任何.NET编程语言写的)中的任何类或结构来实现。这样的话,接口就有较高的多态性。
如果研究.NET Framework 4.5 SDK文档的话,就会发现非常多看似无关的类型(System.Array、System.Data.SqlClient.SqlConnection、System.OperatingSystem、System.String等)都实现了这个接口。尽管这些类型不具有相同的父类(除了System.Object之外),我们可以通过IConeable接口类型把它们当成多态处理。
例如,如果我们有一个含IConeable接口参数的方法CloneMe(),我们就可以把任何实现这个接口的对象传给这个方法:
namespace ICloneableExample { class Program { static void Main( string[] args ) { Console.WriteLine("***** A First Look at Interfaces *****\n"); // 所有这些类都支持ICloneable接口 string myStr = "Hello"; OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version()); System.Data.SqlClient.SqlConnection sqlCnn = new System.Data.SqlClient.SqlConnection(); // 因此,它们就可以传入接口ICloneable的方法 CloneMe(myStr); CloneMe(unixOS); CloneMe(sqlCnn); Console.ReadLine(); } private static void CloneMe( ICloneable c ) { // 克隆我们获得的并输出名字 object theClone = c.Clone(); Console.WriteLine("Your clone is a: {0}", theClone.GetType().Name); } } }
传统抽象基类的另外一个限制就是每一个派生类型必须处理这一组抽象成员并且提供实现。为了演示这个问题,假设我们在Shape基类中新定义了一个叫GetNumberOfPoints()的抽象方法,它允许派生类型返回渲染图形所需的顶点数:
abstract class Shape { //每一个派生类型都必须支持这个方法 public abstract byte GetNumberOfPoints(); }
显然,只有Hexagon类型才拥有顶点。然而,这样更新后,所有派生类型(Circle、Hexagon以及ThreeDCircle)现在都必须提供这个方法完整的实现,即使这么做没有什么意义。同样,接口类型提供了解决方案。如果我们定义了一个接口来表示"有顶点"这个行为,我们就可以把它插到Hexagon类型中,Circle和ThreeDCircle则不受影响。
定义自定义接口
- 接口使用C# interface关键字来定义
- 接口不指定基类(即使是System.Object;然而,接口可以指定基接口)
- 接口的成员不指定访问修饰符(因为所有接口成员都是隐式公共的和抽象的)
这里是一个使用C#定义的自定义接口:
// 这个接口定义了"具有顶点"的行为 public interface IPointy { // 隐式公共的和抽象的 byte GetNumberOfPoints(); }
记住,我们定义接口成员时,不需要为这个成员定义实现作用域。接口是纯粹的协议,因此也不会定义实现(留给支持的类或结构)。因此,如下版本的IPointy会导致各种编译器错误:
// 内有大量错误 public interface IPointy { // 错误!接口不能有字段 public int numbOfPoints; // 错误!接口不能有构造函数 public`IPonity() { numbOfPoints = 0; } // 错误!接口不能提供实现 public int GetNumberOfPoints() { return numbOfPoints; } }
不管怎么样,原始的IPointy接口定义了一个方法。然而,.NET接口还可以定义许多属性协议。例如,我们可以使用只读属性而不是其他的访问方法来创建IPointy接口:
public interface IPointy { // 在接口中的读写属性差不多是 // retType PropName { get; set; } // 而接口中的只写属性是 // retType PropName { set; } byte Points { get; } }
接口类型还可以包含事件以及索引器定义。
接口类型就其本身而言没什么用,因为它们只是抽象成员的集合。例如,我们不能像类和结构一样分配接口类型:
// 分配接口类型是不合法的 static void Main(string[] args) { IPointy p = new IPointy(); // 编译器错误 }
除非被类或结构实现,否则接口没有什么用。在这里,IPointy是一个表示"有顶点"这一行为的接口。原因很简单,图形层次结构中的一些类有顶点(如Hexagon),而其他一些则没有(比如Circle)。
实现接口
如果类(或结构)选择通过支持接口来扩展功能,就需要在其类型定义中使用逗号分隔的列表。要知道直接基类必须是冒号操作符后的第一个项。如果类类型从System.Object直接继承,我们完全可以只在列表中提供类支持的接口,因为如果没有特别指明,C#编译器会从System.Object扩展我们的类型。由于结构总是从System.ValueType继承,只需要在结构定义后直接列出每一个接口就行了。
实现接口是一个“要么全要要么全不要”的命题,也就是说支持类型无法选择实现哪些成员。
现在总结一下,下图显示的Visual Studio类结构图使用流行的“棒棒糖”符号描述了与IPointy兼容的类。注意,Circle和ThreeDCircle没有实现IPointy,因为这个行为对这些特殊类没有意义。Shape层次结构(包含接口):
在对象级别调用接口
现在已经有了一组支持IPointy接口的类,接下来的问题就是如何使用这些新功能。与给定接口功能最直接的交互方式就是直接在对象级别调用方法(所提供的接口成员不是显式实现的)。
static void Main(string[] args) { // 调用IPointy定义的Points属性 Hexagon hex = new Hexagon(); Console.WriteLine("Ponits:{0}", hex.Points); Console.ReadLine(); }
由于读者清楚六边形(Hexagon)类型已经实现了该接口,有了Points属性,所以在本例中这样做没有任何问题。但在其他情况下,读者可能无法在编译时判断指定类型支持哪个接口。例如,假定读者有一个包含50个Shape兼容类型的数组,其中仅有部分数组支持IPointy接口。很明显,如果试图在没有实现IPointy接口的类型中调用Points属性,将收到编译错误。接下来的问题就是:如何才能动态判断一个类型支持哪些接口呢?
在运行时判断一个类型是否支持一个指定接口的一种方式是使用显示强制转换。如果这个类型不支持被请求的接口,将收到一个无效转换异常(InvalidCastException)。使用结构化异常处理妥善处置这种可能的异常,例如:
static void Main(string[] args) { // 捕获可能发生的InvalidCastException异常 Circle c = new Circle("Lisa"); IPointy itfPt = null; try { itfPt = (IPointy)c; Console.WriteLine(itfPt.Points); } catch (InvalidCastException e) { Console.WriteLine(e.Message); } Console.ReadLine(); }
使用try/catch逻辑并非是最好的解决方法,在首次调用该接口成员之前判断其支持哪个接口更加理想。下面介绍两种实现方式。
获取接口引用:as关键字
判断一个指定类型是否支持一个接口的第二种方式就是使用as关键字。如果该对象可被视为一个指定的接口,你可以在使用该关键字的语句中得到指向该对象接口的引用;否则,将返回一个null的空引用。因此,首先要检查null值:
static void Main(string[] args) { // 能将六角形hex2视为实现了IPointy接口吗 Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy; if (itfPt2 != null) { Console.WriteLine("Points:{0}", itfPt2.Points); } else { Console.WriteLine("OOPS! Not pointy..."); } Console.ReadLine(); }
请注意,当使用as关键字的时候,无需使用try/catch逻辑。如果引用非空,说明调用的是一个正确的接口引用。
获取接口引用:is关键字
还可以通过使用is关键字来检查是否实现一个接口。如果要考查的对象与指定接口不符,将返回false值。反之,如果该类型与指定接口相符,就可以安全地调用这些成员,而不必使用try/catch逻辑。
假定更新了Shape类型的数组,使其中部分成员实现了IPointy接口:
static void Main(string[] args) { // 生成Shape数组 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; for (int i = 0; i < myShapes.Length; i++) { // 回调Shape基类定义一个抽象的Draw()成员,由此所有Shape都知道如何绘制自己 myShapes[i].Draw(); // 哪些是有棱角的? if (myShapes[i] is IPointy) Console.WriteLine("-> Points: {0}", ((IPointy)myShapes[i]).Points); else Console.WriteLine("-> {0}\'s not pointy!", myShapes[i].PetName); Console.WriteLine(); } Console.ReadLine(); }
接口作为参数
既然接口是有效的.NET类型,读者可以构造将接口作为参数的方法,对于当前的示例,假定已经定义了另一个名为IDraw3D的接口:
// 模拟能以绝佳3D效果呈现一个类型的能力 public interface IDraw3D { void Draw3D(); }
接下来,假定3种图形中的2种(ThreeDCircle与Hexagon)已经被设定为支持这种新的行为:
// Circle支持IDraw3D接口 class ThreeDCircle : Circle, IDraw3D { ... public void Draw3D() { Console.WriteLine("Drawing Circle in 3D!"); } } // Hexagon支持IPointy与IDraw3D接口 class Hexagon : Shape, IPointy, IDraw3D { ... public void Draw3D() { Console.WriteLine("Drawing Hexagon in 3D!"); } }
新的Visual Studio类图:
如果读者现在定义一个将IDraw3D接口作为参数的方法,将能有效传递任何实现IDraw3D接口的对象(如果读者视图传进一个不支持该接口的类型,将收到编译错误):
// 绘制任何支持IDraw3D接口的类型 static void DrawIn3D(IDraw3D itf3d) { Console.WriteLine("-> Drawing IDraw3D compatible type"); itf3d.Draw3D(); }
可以测试Shape数组中的项是否支持接口,如果支持,就将其传入DrawIn3D()方法:
static void Main(string[] args) { // 生成Shape数组 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; for (int i = 0; i < myShapes.Length; i++) {
... // 支持绘制为3D吗? if (myShapes[i] is IDraw3D) DrawIn3D((IDraw3D)myShapes[i]); } Console.ReadLine(); }
接口作为返回值
例如,可以写一个接受Shape对象数组作为参数、返回支持IPointy的第一项的引用的方法:
static void Main(string[] args) { // 构建Shape数组 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; // 获取第一个pointy项 IPointy firstPointyItem = FindFirstPointyShape(myShapes); Console.WriteLine("The item has {0} points", firstPointyItem.Points); Console.ReadLine(); } static IPointy FindFirstPointyShape(Shape[] shapes) { foreach (Shape s in shapes) { if (s is IPointy) return s as IPointy; } return null; }
接口类型数组
要理解的是,同样的接口即使不在同一个类层次结构,也没有除System.Object以外的公共父类,也可以由多个类型实现,这可以派生出去多非常强大的编程结构。例如,假设我们要在当前项目中开发三个全新的类类型来对厨具(通过Knife和Fork类)和园艺设备(PitchFork,指干草叉)建模:
接口可以插入到类层次结构任何部分的类型中。
如果已经定义了PitchFork、Fork和Knife类型,那么现在可以定义一个支持IPointy接口的对象数组。既然这些成员都支持同样的接口,因此可以抛开类层次结构的全部差异性,通过数组进行迭代并将每个对象视为支持IPointy接口的对象:
static void Main(string[] args) { // 这个数组仅仅包含实现了IPointy接口的类型 IPointy[] myPointyObjects = {new Hexagon(), new Knife(), new Triangle(), new Fork(), new PitchFork()}; foreach (IPointy i in myPointyObjects) Console.WriteLine("Object has {0} points.", i.Points); Console.ReadLine(); }
下面强调一下这个示例的重要性,请记住:如果你有一个给定接口的数组,那么这个数组可以包含实现了该接口的任何类或者结构。
显示接口实现
一个类或结构可以实现许多接口。因此,我们可能实现包含重复命名成员的接口,所以就需要处理命名冲突。现在设计3个自定义接口来表示实现类型呈现自身输出的各种位置:
// 绘制到表单上 public interface IDrawToForm { void Draw(); } // 绘制到内存中 public interface IDrawToMemory { void Draw(); } // 呈现到打印机 public interface IDrawToPrinter { void Draw(); }
注意,每一个接口都定义了Draw()方法,其名称相同(碰巧都没有参数)。如果我们现在希望一个名为Octagon的类类型支持这些接口中的每一个,编译器会允许如下的定义:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { public void Draw() { // 共享绘制逻辑 Console.WriteLine("Drawing to a printer..."); } }
尽管这段代码可以通过编译,你可能也认为我们会有问题。简单来说,如果提供一个Draw()方法实现,我们就不能从Octagon对象根据某个接口采取一系列行为。例如,下面的代码会调用相同的Draw()方法,而不管我们获取到哪个接口:
static void Main(string[] args) { Console.WriteLine("***** Fun with Interface Name Clashes *****\n"); // 所有这些调用都会调用相同的Draw()方法 Octagon oct = new Octagon(); IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw(); IDrawToPrinter itfPrinter = (IDrawToPrinter)oct; itfPrinter.Draw(); IDrawToMemory itfMemory = (IDrawToMemory)oct; itfMemory.Draw(); Console.ReadLine(); }
显然,把图像呈现到窗体的代码和把图像呈现到网络打印机或内存中某个区域的代码不太一样。如果要实现具有相同成员的接口,可以使用显示接口实现语法来解决这种命名冲突:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { // 对某个接口显示绑定Draw() void IDrawToForm.Draw() { Console.WriteLine("Drawing to form..."); } void IDrawToMemory.Draw() { Console.WriteLine("Drawing to memory..."); } void IDrawToPrinter.Draw() { Console.WriteLine("Drawing to a printer..."); } }
我们可以看到,如果显式实现接口成员的话,大致模式可以归结为:returnType InterfaceName.MethodName(params){}
显示实现的成员是自动私有的。
由于显式实现成员总是隐式私有的,这些成员在对象级别就不可用。我们必须使用显示转换来访问需要的功能。例如:
static void Main(string[] args) { Console.WriteLine("***** Fun with Interface Name Clashes *****\n"); Octagon oct = new Octagon(); // 现在必须使用转换来访问Draw()成员 IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw(); // 如果以后不需要使用接口变量,可以简化成这个形式 ((IDrawToPrinter)oct).Draw(); // 也可以使用"as"关键字 if (oct is IDrawToMemory) ((IDrawToMemory)oct).Draw(); Console.ReadLine(); }
虽然这个语法对解决命名冲突很有用,但是如果希望从对象级别隐藏"高级"成员的话,我们也可以使用显示接口实现。这样,如果对象用户使用点操作符的话,他就只能看到类型所有功能的子集。然而,那些需要更多高级行为的人可以通过显示转换提取需要的接口。
设计接口层次结构
接口可以组织成接口层次结构。和类层次结构相似,如果接口扩展了既有接口,它就继承了父类定义的抽象成员。当然,和基于类的继承不同的是,派生接口不会继承真正的实现,而只是通过额外的抽象成员扩展了其自身的定义。
如果希望扩展既有接口功能又不变动既有代码,接口层次结构就会很有用。现在,重新设计之前的一组与呈现相关的接口,这样IDrawable就是家族树的根:
public interface IDrawable { void Draw(); }
由于IDrawable定义了基本绘制行为,我们现在就可以创建派生接口来扩展以修改后的格式呈现的能力,例如:
public interface IAdvancedDraw : IDrawable { void DrawInBoundingBox( int top, int left, int bottom, int right ); void DrawUpsideDown(); }
有了这样的设计,如果一个类实现IAdvancedDraw,我们现在就必须实现在继承链上定义的每一个成员(更准确地说是Draw()、DrawInBoundingBox()和DrawUpsideDown()方法):
public class BitmapImage : IAdvancedDraw { public void Draw() { Console.WriteLine("Drawing..."); } public void DrawInBoundingBox( int top, int left, int bottom, int right ) { Console.WriteLine("Drawing in a box..."); } public void DrawUpsideDown() { Console.WriteLine("Drawing upside down!"); } }
现在,使用BitmapImage时,可以在对象级别上调用每一个方法(因为它们都是公有的),也可以通过显示转换提取每一个支持接口的引用:
static void Main( string[] args ) { Console.WriteLine("***** Simple Interface Hierarchy *****"); // 从对象级别调用 BitmapImage myBitmap = new BitmapImage(); myBitmap.Draw(); myBitmap.DrawInBoundingBox(10, 10, 100, 150); myBitmap.DrawUpsideDown(); // 显示获取IAdvancedDraw IAdvancedDraw iAdvDraw = myBitmap as IAdvancedDraw; if (iAdvDraw != null) iAdvDraw.DrawUpsideDown(); Console.ReadLine(); }
接口类型的多重继承
和类类型不同,一个接口可以扩展多个基接口。这就允许我们设计非常强大、非常灵活的抽象。
// 接口可以是多重继承的 interface IDrawable { void Draw(); } interface IPrintable { void Print(); void Draw(); // <-- 注意,可能导致命名冲突 } // 多重接口继承。没有问题 interface IShape : IDrawable, IPrintable { int GetNumberOfSides(); }
接口层次结构:
现在,关键问题就是如果我们有一个类支持IShape,需要实现多少方法呢?回答是:看情况。如果希望提供Draw()的简单实现,只需要提供3个成员,如下Rectangle类型所示:
class Rectangle : IShape { public int GetNumberOfSides() { return 4; } public void Draw() { Console.WriteLine("Drawing..."); } public void Print() { Console.WriteLine("Prining..."); } }
如果我们更愿意对每一个Draw()方法提供特定实现(这里应该比较有意义的),就可以使用显示接口实现解决命名冲突,如下面的Square类型所示:
class Square : IShape { // 使用显式实现来处理成员命名冲突 void IPrintable.Draw() { // 绘制到打印机上 } void IDrawable.Draw() { // 绘制到屏幕上 } public void Print() { // 打印 } public int GetNumberOfSides() { return 4; } }