例如,IComparable 使得类型可以比较大小。接口是一种特殊的抽象类,在其中,不能显式地实现方法;也就是说,接口只拥有一组方法的签名。
不过,这个看似已经成为常识的事情在 C# 8 中可能会发生改变:接口也许可以拥有默认方法实现。如果这真的变成了现实,那么,C# 就成了一个允许多重继承的语言。
当然,不是继承了接口就万事大吉的,必须实现接口的方法才可以真正的扩展你的类。例如,必须实现 CompareTo,并在其中解释如何比较大小。
接口负责对现实生活中 Has A 关系的描述。例如,可以比较大小明显是某个类拥有(has)的性质或能力,而不是说,某个类是“比较大小”,这在中文上都不通。
接口中的所有方法都是 public 的,也不能再使用其他访问修饰符进行修饰。接口也可以拥有自己的属性、事件和索引器。
显式接口实现
如果两个不同的接口中含有一个相同的方法(相同的名称,参数和返回类型),那么,同时继承这两个接口的类型在实现该方法之后,两个接口的方法就会具有相同的行为:
class Program { static void Main(string[] args) { var cg = new Greeting(); var chinese = (ChineseGreeting)cg; //你好 chinese.Hello(); var english = (EnglishGreeting)cg; //你好 english.Hello(); Console.ReadKey(); } } public interface ChineseGreeting { void Hello(); } public interface EnglishGreeting { void Hello(); } public class Greeting : ChineseGreeting, EnglishGreeting { //隐式接口实现导致方法只有一次实现机会 //两个接口的Hello将会有相同的行为 public void Hello() { Console.Write("你好!"); } }
为了区别开不同接口相同方法的实现,我们可以使用显式接口实现:
public class Greeting : ChineseGreeting, EnglishGreeting { //显式接口实现 void ChineseGreeting.Hello() { Console.Write("你好!"); } void EnglishGreeting.Hello() { Console.Write("Hello!"); } }
用的方式是相同的。在使用显式接口实现之后,必须将对象转化为接口类型才可以访问它。因为显式接口实现的方法默认是接口类型私有的(也不能使用访问修饰符),而且是 sealed。
下面的代码试图从接口的继承类型调用显式接口实现的方法,这会导致编译错误:
//继承类不存在此方法,显式接口实现方法归接口私有
cg.Hello();
显式接口实现有着诸多缺点,例如必须手动转换类型为接口基类型(如果派生类是值类型,那么还会导致装箱),没有 IDE 智能感知等。
下面是一个 .NET 中的显式接口实现的例子:
int 1=1;
//由于 ToSingle 是显式接口实现,所以必须先转化为接口基类型
var s = ((IConvertible) i).ToSingle(null);
直接在 i 上调用 ToSingle 无法通过编译。我们看看 int 中的相关代码:
float IConvertible.ToSingle(IFormatProvider provider);
ToSingle 签名包含了接口类型 IConvertible,说明了 ToSingle 方法使用了显式接口实现。
显式接口实现与泛型接口
在泛型还没出现的年代,实现一个接口有时是很头疼的。例如,常用的 IComparable 接口中的 CompareTo 方法,输入参数是 object。
因此,任何输入都合法,这不仅可能带来装箱,还会在内部重新转化为自己想要的类型时发生错误。
显式接口实现可以在一定程度上避免这个问题:
public class People : IComparable { public string name { get; set; } public int age { get; set; } //类型安全的CompareTo方法 public int CompareTo(People p) { return p.age >= age ? 1 : -1; } //为了实现ICompareable,不得不显示接口实现 int IComparable.CompareTo(object obj) { //obj转化为People类型可能会出现问题 return CompareTo((People)obj); } }
上面的类型中,通过比较年龄来比较 People 类的大小。在接受 object 为参数的 CompareTo 方法中,必须强制将 obj 转化为 People 类,而这是类型不安全的。
我们通过自己写一个 CompareTo 方法,并令输入为 People 类解决这个问题。
但是自己的 CompareTo 方法并不算是 IComparable 的实现,我们还要被迫实现一个 IComparable 规定的 CompareTo 方法。
People p = new People();
People q = new People();
q.CompareTo(p);
//如果对象是 People 类型,访问不到显式接口实现的 CompareTo 方法
//也就不能传入任意类型
q.CompareTo(i); //不能通过编译!
//但是可以通过将类型转化为接口基类型
//访问到显式接口实现的 CompareTo 方法,传入任意类型
//于是再次失去类型安全性
var q2 = (IComparable) q;
q2.CompareTo(i); //可以通过编译!
我们看到,显式接口实现还是不能完全保证类型安全性,当泛型接口出现之后,问题 就解决了:
public class People : IComparable<People> { public string name { get; set; } public int age { get; set; } //类型安全的CompareTo方法 public int CompareTo(People p) { return p.age >= age ? 1 : -1; } }
继承 IComparable<T> 使得我们不需要再写一个输入参数为 object 的 CompareTo 方法,它绝对保证类型安全,而且,由于没有类型转换,也就不存在装箱。
抽象类 VS 接口
接口和抽象类有一些区别,接口是一种特殊的抽象类:
- 接口不能有非抽象的方法。而抽象类可以有非抽象的方法。
- 接口和抽象类都不能被实例化。
- 如果继承接口,必须实现接口所有的抽象方法。而继承抽象类的类可以只实现一部分父类的抽象方法。
- 一个类可以继承多个接口,但只能继承自一个别的类。
- 不能密封抽象类和接口,因为这破坏了接口和抽象类本身的性质,即被人继承。
抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是它的子类应该实现的一组规则。当逻辑为一般和特殊的关系,而特殊本身无法或无需有任何方法的实现时,则考虑使用抽象类。
当逻辑为“能做什么”时,使用接口。这样的例子太多了,例如 C# 中所有的数据结构都继承接口 IEnumerable,意味着所有的数据结构都拥有枚举的能力。
另外,如果你的类型继承了接口 IDisposable,则拥有 Dispose 的能力。
如果基类型发生变化(例如添加了方法),那么派生类型将自动继承该方法,不需要更改代码。
但如果接口添加了方法,那么所有派生类型都必须更改代码才能通过编译。在这种情形之下,要不就老老实也改代码,要不就考虑将新方法做成一个新的接口。
接口不继承自 Object
接口不继承自任何类型,它旨在提供一个干净的方法库和固定的签名,并等待继承者来实现。
如果接口继承自 Object,那么它就会具有 Object 的方法了。但是,任何类型其实都已经继承了 Object,所以类型再继举一个继承了 Object 方法的接口,就会导致毫无意义的重复。
接口方法分派
假设有一个接口 ITest,含有方法A,—个类型 B 继承了该接口,并实现了 B.A,那么,实际上 B.A 是被 IL 的 newslot+virtual 修饰的(显式接口实现的话,则再加一个 final 关键字)。
因此,可以在类型 B 的方法表中找到 B.A,然后进行调用。而相对的,虽然接口 ITest 是类型 B 的父类,而且方法 A 隐式的为 virtual,接口方法 ITest.A 是不会出现在类型 B 的方法表中的。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/22373.html