第8章 接口
并非只能通过继承实现多态性,还能通过接口实现。和抽象类不同,接口不包含任何实现,但和抽象类相似,接口也定义了一组成员,调用者可认为这些成员已实现。
类型通过实现接口来定义其功能,接口实现关系是一种“能做==”can do关系==:类型能做接口所规定的事情。在实现接口的类型和使用接口的代码之间,接口订立了契约。实现接口的类型必须使用接口要求的签名来定义方法。
8.1 接口概述
interface IFileCompresshion
{
void Compress(string targetFileName,string[] fileList)
void Uncompress(string compressedFileName,string expandDirectoryName);
}
初学者主题:为什么需要接口
接口有用是因为和抽象类不同,它能完全隔离实现细节和提供的服务。接口就像电源插座。电如何输送到插座是实现细节;电器不必关心电如何输送到插座,秩序提供兼容的插头。
接口的强大之处在于,调用者可随便切换不同的实现而不需要修改调用代码。
接口的关键之处是不包含实现和数据,注意其中的方法声明用分号取代了大括号。字段不能在接口声明中出现。如接口要求派生类包含特定数据,会声明属性而不是字段。
接口声明的成员描述了在实现该接口的类型中必须能访问的成员。而所有非公共成员的目的都是组织其他代码访问成员,所以,C#不允许为接口成员使用访问修饰符,所有成员都自动公共。
设计规范
•接口名称使用Pascal大小写,加“I”前缀。
8.2 使用接口实现多态性
public interface IShape
{
double GetArea();
}
public class Circle : IShape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public double GetArea()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public double GetArea()
{
return Width * Height;
}
}
// Usage
IShape shape1 = new Circle(5);
IShape shape2 = new Rectangle(4, 6);
double area1 = shape1.GetArea(); // Returns the area of the circle
double area2 = shape2.GetArea(); // Returns the area of the rectangle
8.3 接口实现
声明类来实现接口类似于从基类派生——要实现的接口和基类名称以逗号分隔。类可实现多个接口,但只能从一个基类派生。
实现接口时,接口的所有成员都必须实现,抽象类既可以将接口方法映射为抽象方法,也可以将它实现为非抽象方法并在方法中抛出NotImplementedException异常,但无论如何都要提供接口成员的一个“实现”。
interface IFoo
{
void Bar();
}
在抽象类中可将接口方法映射成自己的抽象方法,将真正的实现留给子类完成:
abstract class Foo:IFoo
{
public abstract void Bar();
}
也可拿掉abstract关键字并添加方法主体:
abstract class Foo:IFpp
{
public void Bar();
{
throw new NotImplementedException();
}
}
接口的重点在于永远不能实例化,即不能用new创建接口。所以接口没有构造函数或终结器。只有实例化实现了接口的类型,才能使用接口实例。此外接口不能包含静态成员。接口为多态性而生,而假如没有实现接口的那个类型的实例,多态性就没什么价值。
每个接口成员的行为和抽象方法相似,都是强迫派生类实现成员,但不能为接口成员显式添加abstract修饰符。
在类型中实现接口成员时有两种方式:显式和隐式。之前看到的是隐式实现,是用类型的公共成员实现接口成员。
8.3.1 显式成员实现
// Justification: Only a aartial implmentation provided for elucidation purposes.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter08.Listing08_04
{
using System;
using Listing08_02;
public class Program
{
public static void Main()
{
string?[] values;
Contact contact = new Contact("Inigo Montoya");
// ...
// ERROR: Unable to call .CellValues directly
// on a contact
// values = contact.CellValues;
// First cast to IListable
values = ((IListable)contact).CellValues;//执行强制转型和调用CellValues
// ...
}
}
public class Contact : PdaItem, IListable
{
// ...
public Contact(string name)
: base(name)
{
}
#region IListable Members
string?[] IListable.CellValues
{
get
{
return new string?[]
{
FirstName,
LastName,
Phone,
Address
};
}
}
#endregion
private string? _LastName;
protected string LastName
{
get => _LastName!;
set => _LastName = value ?? throw new ArgumentNullException(nameof(value));
}
private string? _FirstName;
protected string FirstName
{
get => _FirstName!;
set => _FirstName = value ?? throw new ArgumentNullException(nameof(value));
}
protected string? Phone { get; set; }
protected string? Address { get; set; }
static public string GetName(string firstName, string lastName)
=> $"{ firstName } { lastName }";
}
}
显式成员实现的方法只能通过接口本身调用,最典型的做法是将对象转型为接口。
通过属性名附加IListable前缀来显式实现ColumnValues。
8.3.2 隐式成员实现
result = Contact1.CompareTo(contact2);
8.3.3 显式和隐式接口实现的比较
对于隐式和显式实现的接口成员,关键区别不在于成员声明的语法,而在于通过类型的实例而不是接口访问成员的能力。
建立类层次接口时需要建模真实世界的“属于” is a关系——例如,长颈鹿属于哺乳动物。而接口用于建模机制关系。
一般来说,最好的做法是将一个类的公共层面限制成“全模型”,尽量少地涉及无关的机制;遗憾的是,有的机制在.NET中是不可避免的。不能获得长颈鹿的哈希码,或者将长颈鹿转换成字符串。但可获得长颈鹿类的哈希码,并把它转换为字符串。
设计规范
避免显式实现接口成员,除非有很好的理由。
8.4 在实现类和接口之间转换
类似于派生类和基类的关系,实现类可隐式转换为接口,无须转型操作符。实现类的实例总是包含接口的全部成员,所以总是能成功转换为接口类型。
虽然从实现类型向接口的转换总是成功,但可能有多个类型实现了同一个接口。所以,无法保证从接口向实现类型的向下转型能成功,接口必须显式转型为它的某个实现类型。
8.5 接口继承
一个接口可以从另一个接口派生,派生的接口将继承”基接口“的所有成员。
即使类实现的是从基接口派生的接口,仍可明确声明自己要实现这两个接口。
最后要说的是,虽然”继承“这个词用得没错,但更准确的说法是接口代表契约,一份契约可指定另一份也必须遵守的条款。
8.6 多接口继承
就像类能是实现多个接口一样,接口也能从多个接口继承,而且语法和类的继承与实现语法一致。很少有接口没有成员,但如果要求同时实现两个接口,这种情况就很正常了。
8.7 接口上的扩展方法
扩展方法的重要特点是除了能作用于类,还能作用于接口。语法和作用于类时一样。
8.8 通过接口实现多继承
设计规范
•考虑定义接口获得和多继承相似的效果
8.9 版本控制
创建新版时不要修改接口,如组件或应用程序正在供其他开发者使用。一个接口一旦被发布,就不可以再被修改。
设计规范
不要为已交付的接口添加成员
8.9.1 C#8.0之前和之后的接口版本升级
8.10 比较接口和类
接口引入了另一个类别的数据类型,但和类不同,接口永远不能实例化,只能通过对实现接口的一个对象的引用来访问接口实例。不能使用new操作符创建接口实例,所以接口不能包含任何构造函数或终结器。此外,接口不允许静态成员。
接口类似于抽象类,有一些共同特点,比如都缺少实例化能力。
表8.1 抽象类和接口的比较
抽象类 | 接口 |
---|---|
不能直接实例化,只能实例化派生类 | 不能直接实例化,只能实例化一个实现类型 |
派生类要么自己也是抽象的,要么必须实现所有抽象成员 | 实现类型必须实例化所有接口成员 |
可添加额外的非抽象成员,由所有派生类继承,不会破坏跨版本的版本兼容性 | 为接口添加额外的成员会破坏版本兼容性 |
可声明方法、属性和字段以及其他成员类型包括析构函数和终结器 | 可声明方法和属性但不能声明字段、构造函数或终结器。 |
成员可以是实例、虚、抽象或静态、非抽象成员可提供默认实现供派生类使用 | 所有成员都基于实例,而且自动视为抽象,所以不能包含任何实现 |
派生类只能从一个基类派生,单继承 | 实现类型可实现任意多的接口 |
设计规范
•一般要优先选择类而不是接口,用抽象类分离契约(类型做什么)与实现接口(类型怎么做)
•如果需要使已从其他类型派生的类型支持接口定义的功能,考虑定义接口
8.11 比较接口和特性
有时用无任何成员的接口(不管是不是继承)来描述关于类型的信息,一般认为这是对接口机制的滥用:接口应表示类型能执行的功能,而非陈述关于类型的事实。
设计规范
•避免使用无成员的标记接口,改为使用特性。