其实称本篇为多态还是有些牵强,因为在类的继承中也是存在多态的,例如我们的重写机制,但可以设想这样一个场景:飞行这个动作,鸟可以飞行,飞机可以飞行,而飞机其实和鸟没有父子关系的,他们共同拥有的是行为:飞行。所以本篇博客着重介绍这一点:如何通过接口来处理行为一致(横向关系)而非一脉相承(纵向关系)的关系。本篇的结构如下:
接口定义
为什么要有接口?在介绍了类和抽象类(我的感觉就是抽象类属于类和接口的中间产物,既有继承基类的能力,也有扩展抽象成员方法的能力)之后,我们知道,可以通过重写和实现抽象成员来扩展和组织自己的内容。那么接口有啥必要呢?,既然有类,那还要接口做什么呢?:
- 接口不包含任何实现,可以完全隔离实现细节和提供的服务
- 基类除了允许共享成员签名,还可以共享实现(派生类通过继承基类方法直接使用其实现),接口只允许共享成员签名,不允许共享实现(接口本来就不提供实现)。
- C#是单继承的,可以通过使用接口来扩展想要的实现方法,而且接口是多继承(实现)的。
接口的定义如下,
interface IFileCompression { void Compress(string targetFileName, string[] fileList); void Uncompress(string compressedFileName, string expandDirectoryName); }
需要注意以下几点:
- 接口不包含任何实现和数据,只包含属性(属性也不包含对应的支持字段,所以不能使用自动属性)和实例方法,并且不包含任何静态成员!
- C#不允许接口使用访问修饰符,所有成员自动公共。
- 派生类实现接口时,也要用冒号,和基类以逗号分隔,需要注意的是,基类在前,接口顺序任意.
- 接口的所有成员都必须实现。
- 接口永远不能实例化,而且接口不像抽象类,连构造函数和终结器都没有。
- 接口必须显示转换为它的某个实现类型,而因为它的派生类总是实现它的全部方法,所以可以隐式转换。
- 不要修改已经发布的接口,而是通过给该接口添加子接口来实现版本控制
抽象类如何通过接口甩锅呢?通过如下方式抽象类可以不实现接口方法而是让派生类实现。
public interface Ifoo { void Bar(); } public abstract class Foo : Ifoo { public abstract void Bar(); } public class Child : Foo { public override void Bar() { throw new NotImplementedException(); } }
接口实现多态
如何使用接口实现多态,其实和隐式转型类似哦。
1,定义接口和抽象类
public interface IListable { // Return the value of each column in the row string[] ColumnValues { get; } } public abstract class PdaItem { public PdaItem(string name) { Name = name; } public virtual string Name { get; set; } }
2,定义两种不同的实现类
public class Contact : PdaItem, IListable { public Contact(string firstName, string lastName, string address, string phone) : base(null) { FirstName = firstName; LastName = lastName; Address = address; Phone = phone; } public string FirstName { get; set; } public string LastName { get; set; } public string Address { get; set; } public string Phone { get; set; } public string[] ColumnValues { get { return new string[] { FirstName, LastName, Phone, Address }; } } public static string[] Headers { get { return new string[] { "First Name", "Last Name ", "Phone ", "Address " }; } } // ... } public class Publication : IListable { public Publication(string title, string author, int year) { Title = title; Author = author; Year = year; } public string Title { get; set; } public string Author { get; set; } public int Year { get; set; } public string[] ColumnValues { get { return new string[] { Title, Author, Year.ToString() }; } } public static string[] Headers { get { return new string[] { "Title ", "Author ", "Year" }; } } // ... }
3,定义接口调用方的类和调用方法
public class ConsoleListControl { public static void List(string[] headers, IListable[] items) { int[] columnWidths = DisplayHeaders(headers); for(int count = 0; count < items.Length; count++) { string[] values = items[count].ColumnValues; DisplayItemRow(columnWidths, values); } } }
4,定义入口,方法实现
public static void Main() { Contact[] contacts = new Contact[] { new Contact( "Dick", "Traci", "123 Main St., Spokane, WA 99037", "123-123-1234"), new Contact( "Andrew", "Littman", "1417 Palmary St., Dallas, TX 55555", "555-123-4567"), new Contact( "Mary", "Hartfelt", "1520 Thunder Way, Elizabethton, PA 44444", "444-123-4567"), new Contact( "John", "Lindherst", "1 Aerial Way Dr., Monteray, NH 88888", "222-987-6543"), new Contact( "Pat", "Wilson", "565 Irving Dr., Parksdale, FL 22222", "123-456-7890"), new Contact( "Jane", "Doe", "123 Main St., Aurora, IL 66666", "333-345-6789") }; // Classes are cast implicitly to // their supported interfaces ConsoleListControl.List(Contact.Headers, contacts); Console.WriteLine(); Publication[] publications = new Publication[3] { new Publication( "The End of Poverty: Economic Possibilities for Our Time", "Jeffrey Sachs", 2006), new Publication("Orthodoxy", "G.K. Chesterton", 1908), new Publication( "The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 1979) }; ConsoleListControl.List( Publication.Headers, publications); }
隐式调用接口方法,会使用各个派生类对接口各自的实现方式:
Contact会实现如下内容:输出以下四个属性:FirstName、LastName、Phone、Address
Publication会实现如下内容:输出:Title、Author、Year
接口实现
分为显式和隐式、接口继承,以及多接口继承。
显式和隐式
接口其实分为两种实现,显式和隐式,上面介绍的实现是隐式的,感觉像是我们大多数时候使用的场景,但有些时候也会用到显式实现:
public class Program { public static void Main() { string[] values; Contact contact1, contact2 = null; // ... // ERROR: Unable to call ColumnValues() directly // on a contact // values = contact1.ColumnValues; // First cast to IListable values = ((IListable)contact2).ColumnValues;// 显式 var result = contact1.CompareTo(contact2);// 隐式 } } public class Contact : PdaItem, IListable, IComparable { // ... public Contact(string name) : base(name) { } #region IComparable Members public int CompareTo(object obj) //隐式实现 { //... } #endregion #region IListable Members string[] IListable.ColumnValues //显式实现 { get { return new string[] { FirstName, LastName, Phone, Address }; } } #endregion protected string LastName { get; set; } protected string FirstName { get; set; } protected string Phone { get; set; } protected string Address { get; set; } }
显式实现: values = ((IListable)contact2).ColumnValues;
,显式实现方式只能通过接口本身调用!不能使用virtual、override或者public来修饰!
隐式实现: var result = contact1.CompareTo(contact2);
隐式实现方式允许类自身调用!virtual、override或者public都是可选参数。
对于隐式和显式实现的接口成员,关键区别不在于成员声明的语法,而在于通过类型的实例而不是接口访问成员的能力。 建立类层次结构时需要建模真实世界的“属于”(is a)关系——例如,长颈鹿“属于”哺乳动物。这些是“语义”(semantic)关系。而接口用于建模“机制”(mechanism)关系。PdaItem“不属于”一种“可比较”(comparable)的东西,但它仍可实现IComparable接口。该接口和语义模型无关,只是实现机制的细节。显式接口实现的目的就是将“机制问题”和“模型问题”分开。要求调用者先将对象转换为接口(比如IComparable),然后才能认为对象“可比较”,从而显式区分你想在什么时候和模型沟通,以及想在什么时候处理实现机制。
一般来说,最好的做法是将一个类的公共层面限制成“全模型”,尽量少地涉及无关的机制
- 成员是不是核心的类功能,如果是,使用隐式,这样把实现方法当成自己成员,如果不是,用显式。
- 接口成员名称作为类成员名称是否恰当?假定ITrace接口的Dump()成员将类的数据写入跟踪日志。在Person或者Truck(卡车)类中隐式实现Dump()会混淆该方法的作用。所以,更好的选择是显式实现,确保只能通过ITrace数据类型调用Dump(),使该方法不会产生歧义。总之,假如成员的用途在实现类中不明确,就考虑显式实现。
- **是否已经有相同签名的类成员?**显式接口成员实现不会在类型的声明空间添加具名元素。所以,如果类型已存在可能冲突的成员,那么显式接口可与之同签名
大多数情况我们使用隐式的方式。
接口继承
接口可以多继承,但是需要注意的是,如果显式的实现接口,则必须使用最初声明该接口的名称。
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter08.Listing08_08 { interface IReadableSettingsProvider { string GetSetting(string name, string defaultValue); } interface ISettingsProvider : IReadableSettingsProvider { void SetSetting(string name, string value); }
这样调用Getting方法是不对的:
class FileSettingsProvider : ISettingsProvider, IReadableSettingsProvider { #region ISettingsProvider Members public void SetSetting(string name, string value) { // ... } #endregion #region IReadableSettingsProvider Members string ISettingsProvider.GetSetting(string name, string defaultValue) { return name + defaultValue; //just returning this for the example } #endregion } }
应该这样调用:
string IReadableSettingsProvider.GetSetting(string name, string defaultValue) { return name + defaultValue; //just returning this for the example }
接口多继承
接口和类一样,也可以继承多个接口:
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter08.Listing08_09 { interface IReadableSettingsProvider { string GetSetting(string name, string defaultValue); } interface IWriteableSettingsProvider { void SetSetting(string name, string value); } interface ISettingsProvider : IReadableSettingsProvider, IWriteableSettingsProvider { } }
接口附加扩展方法
接口也可以被当做扩展方法使用,而且更加灵活,比类更加实用,而且该扩展方法不仅可以是特定类型,还允许特定类型的集合。
static class Listable { public static void List( this IListable[] items, string[] headers) { int[] columnWidths = DisplayHeaders(headers); for(int itemCount = 0; itemCount < items.Length; itemCount++) { if (items[itemCount] != null) { string[] values = items[itemCount].ColumnValues; DisplayItemRow(columnWidths, values); } } } }
接口和类
接口和类的一些比较,用途不同吧:
推荐这么使用二者:
- 一般要优先选择类而不是接口。用抽象类分离契约(类型做什么)与实现细节(类型怎么做)。
- 如果需要使已从其他类型派生的类型支持接口定义的功能,考虑定义接口。
其实二者各有千秋吧,一种是模型角度,一种是机制角度,一种纵向,一种横向。