首页 > 编程笔记 > C#笔记

C# Interface:接口

C# 的接口可以看成是一个“技能库”,继承一个接口可以看成是“插上一个新的技能库”,它使得你的类型拥有更多的“技能”。

例如,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 的方法表中的。

所有教程

优秀文章