C#中的接口默认方法是什么?-创新互联

C#中接口默认方法是什么?相信很多新手小白对C#中接口默认方法的了解处于懵懂状态,通过这篇文章的总结,希望你能有所收获。如下资料是关于接口默认方法的内容。

创新互联是一家专注于成都网站设计、做网站与策划设计,松阳网站建设哪家好?创新互联做网站,专注于网站建设10多年,网设计领域的专业建站公司;建站业务涵盖:松阳等地区。松阳做网站价格咨询:18980820575
interface IStringList {
    void Add(string o); // 添加元素
    void Remove(int i); // 删除元素
    string Get(int i);  // 获取元素
    int Length { get; } // 获取列表长度
}

不管怎么说,这个列表已经拥有了基本的增删除改查功能,比如遍历,可以这样写

IStringList list = createList();
for (var i = 0; i < list.Length; i++) {
    string o = list.Get(i);
    // Do something with o
}

这个IStringList作为一个基础接口在类库中发布之后,大量的程序员使用了这个接口,实现了一堆各种各种各样的列表,像StringArrayListLinkedStringListStringQueueStringStackSortedStringList……有抽象类,有扩展接口,也有各种实现类。总之,经过较长一段时间的积累,IStringList的子孙遍布全球。

然后IStringList的发明者,决定为列表定义更多的方法,以适合在技术飞速发展下开发者们对IStringList使用便捷性的要求,于是

interface IStringList {
    int IndexOf(string o);          // 查找元素的索引,未找到返回 -1
    void Insert(string o, int i);   // 在指定位置插入元素

    // ------------------------------
    void Add(string o); // 添加元素
    void Remove(int i); // 删除元素
    string Get(int i);  // 获取元素
    int Length { get; } // 获取列表长度
}

当然,接口变化之外所有实现类都必须实现它,不然编译器会报错,基础库的抽象类AbstractStringList中实现了上述新增加的接口。整个基础库完美编译,发布了 2.0 版本。

然而,现实非常残酷!

基础库的用户们(开发者)发出了极大的报怨声,因为他们太多代码编译不过了!

是的,并不是所有用户都会直接继承AbstractStringList,很多用户直接实现了IStringList。还有不少用户甚至扩展了IStringList,但他们没有定义int IndexOf(string o)而是定义的int Find(string o)。由于基础库接口IStringList的变化,用户们需要花大量地时间去代码来实现IStringList中定义的新方法。

这个例子是提到了IStringList,只添加了两个方法。这对用户造成的麻烦虽然已经不小,但工作量还算可以接受。但是想想 JDK 和 .NET Framework/Core 庞大的基础库,恐怕用户只能用“崩溃”来形容!

2. 办法

肯定不能让用户崩溃,得想办法解决这个问题。于是,Java 和 C# 的两个方案出现了

  • Java 提出了默认方法,即在接口中添加默认实现
  • C# 提出了扩展方法,即通过改变静态方法的调用形式来假装是对象调用

不得不说 C# 的扩展方法很聪明,但它毕竟不是真正对接口进行扩展,所以在 C# 8 中也加入了默认方法来解决接口扩展造成的问题。

接口扩展方法提出来之后,虽然解决了默认实现的问题,却又带出了新的问题。

  • 接口实现了默认方法,实现接口的类还需要实现吗?如果不实现会怎么样?
  • 无论 Java 还是 C# 都不允许类多继承,但是接口可以。而接口中的默认实现带来了类似于类多继承所产生的问题,怎么办?
  • 在复杂的实现和继承关系中,最终执行的到底会是哪一个方法?

3. 问题一,默认方法和类实现方法的关系

忽略上面IStringList接口中补充的Insert(Object, int)方法,我们把关注点放在IndexOf(Object)上。Java 和 C# 的语法异曲同工:

3.1. 先来看看默认方法的语法

  • Java 版
interface StringList {
    void add(Object s);
    void remove(int i);
    Object get(int i);
    int getLength();

    default int indexOf(Object s) {
        for (int i = 0; i < getLength(); i++) {
            if (get(i) == s) { return i; }
        }
        return -1;
    }
}
  • C# 版
interface IStringList
{
    public void Add(string s);
    void Remove(int i);
    string Get(int i);
    int Length { get; }
    int IndexOf(string s)
    {
        for (var i = 0; i < Length; i++)
        {
            if (Get(i) == s) { return i; }
        }
        return -1;
    }
}

这里把 C# 和 Java 的接口都写出来,主要是因为二者讲法和命名规范略有不同。接下来进行的研究 C# 和 Java 行为相似的地方,就主要以 C# 为例了。

怎么区分是 C# 示例还是 Java 示例?看代码规范,最明显的是 C# 方法用 Pascal 命名规则,Java 方法用 camel 命名规则。当然,还有 Lambda 的箭头也不一样。

接下来的实现,仅以 C# 为例:

class MyList : IStringList
{
    List list = new List();  // 偷懒用现成的

    public int Length => list.Count;
    public void Add(string o) => list.Add(o);
    public string Get(int i) => list[i];
    public void Remove(int i) => list.RemoveAt(i);
}

MyList没有实现IndexOf,但是使用起来不会有任何问题

class Program
{
    static void Main(string[] args)
    {
        IStringList myList = new MyList();
        myList.Add("First");
        myList.Add("Second");
        myList.Add("Third");

        Console.WriteLine(myList.IndexOf("Third"));  // 输出 2
        Console.WriteLine(myList.IndexOf("first"));  // 输出 -1,注意 first 大小写
    }
}

3.2. 在 MyList 中实现 IndexOf

现在,在MyList中添加IndexOf,实现对字符串忽略大小写的查找:

// 这里用 partial class 表示是部分实现,
// 对不住 javaer,Java 没有部分类语法
partial class MyList
{
    public int IndexOf(string s)
    {
        return list.FindIndex(el =>
        {
            return el == s
                || (el != null && el.Equals(s, StringComparison.OrdinalIgnoreCase));
        });
    }
}

然后Main函数中输出的内容变了

Console.WriteLine(myList.IndexOf("Third")); // 还是返回 2
Console.WriteLine(myList.IndexOf("first")); // 返回 0,不是 -1

显然这里调用了MyList.IndexOf()

3.3. 结论,以及 Java 和 C# 的不同之处

上面主要是以 C# 作为示例,其实 Java 也是一样的。上面的示例中是通过接口类型来调用的IndexOf方法。第一次调用的是IStringList.IndexOf默认实现,因为这时候MyList并没有实现IndexOf;第二次调用的是MyList.IndexOf实现。笔者使用 Java 写了类似的代码,行为完全一致。

因此,对于默认方法,会优先调用类中的实现,如果类中没有实现具有默认方法的接口,才会去调用接口中的默认方法。

但是!!!前面的示例是使用的接口类型引用实现,如果换成实例类类型来引用实例呢?

如果MyList中实现了IndexOf,那结果没什么区别。但是如果MyList中没有实现IndexOf的时候,Java 和 C# 在处理上有就区别了。

先看看 C# 的Main函数,编译不过(Compiler Error CS1929),因为MyList中没有定义IndexOf

C#中的接口默认方法是什么?

而 Java 呢?通过了,一如既往的运行出了结果!

C#中的接口默认方法是什么?

从 C# 的角度来看,MyList既然知道有IndexOf接口,那就应该实现它,而不能假装不知道。但是如果通过IStringList来调用IndexOf,那么就可以认为MyList并不知道有IndexOf接口,因此允许调用默认接口。接口还是接口,不知道有新接口方法,没实现,不怪你;但是你明知道还不实现,那就是你的不对了。

但从 Java 的角度来看,MyList的消费者并不一定是MyList的生产者。从消费者的角度来看,MyList实现了StringList接口,而接口定义有indexOf方法,所以消费者调用myList.indexOf是合理的。

Java 的行为相对宽松,只要有实现你就用,不要管是什么实现。

而 C# 的行为更为严格,消费者在使用的时候可以通过编译器很容易了解到自己使用的是类实现,还是接口中的默认实现(虽然知道了也没多少用)。实际上,如果没在在类里面实现,接口文档中就不会写出来相关的接口,编辑器的智能提示也不会弹出来。实在要写,可以显示转换为接口来调用:

Console.WriteLine(((IStringList)myList).IndexOf("Third"));

而且根据上面的试验结果,将来MyList实现了IndexOf之后,这样的调用会直接切换到调用MyList中的实现,不会产生语义上的问题。

4. 问题二,关于多重继承的问题

无论 Java 还是 C# 都不允许类多继承,但是接口可以。而接口中的默认实现带来了类似于类多继承所产生的问题,怎么办?

举个例,人可以走,鸟也可以走,那么“云中君”该怎么走?

4.1. 先来看 C# 的

类中不实现默认接口的情况:

interface IPerson
{
    void Walk() => Console.WriteLine("IPerson.Walk()");
}

interface IBird
{
    void Walk() => Console.WriteLine("IBird.Walk()");
}

class BirdPerson : IPerson, IBird { }

调用结果:

BirdPerson birdPerson = new BirdPerson();
// birdPerson.Walk();           // CS1061,没有实现 Walk
((IPerson)birdPerson).Walk();   // 输出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 输出 IBird.Walk()

不能直接使用birdPerson.Walk(),道理前面已经讲过。不过通过不同的接口类型来调用,行为是不一致的,完全由接口的默认方法来决定。这也可以理解,既然类没有自己的实现,那么用什么接口来引用,说明开发者希望使用那个接口所规定的默认行为。

说得直白一点,你把云中君看作人,他就用人的走法;你把云中君看作鸟,它就用鸟的走法。

然而,如果类中有实现,情况就不一样了:

class BirdPerson : IPerson, IBird
{
    // 注意这里的 public 可不能少
    public void Walk() => Console.WriteLine("BirdPerson.Walk()");
}
BirdPerson birdPerson = new BirdPerson();
birdPerson.Test();              // 输出 BirdPerson.Walk()
((IPerson)birdPerson).Walk();   // 输出 BirdPerson.Walk()
((IBird)birdPerson).Walk();     // 输出 BirdPerson.Walk()

输出完全一致,接口中定义的默认行为,在类中有实现的时候,就当不存在!

云中君有个性:不管你怎么看,我就这么走。

这里唯一需要注意的是BirdPerson中实现的Walk()必须声明为public,否则 C# 会把它当作类的内部行为,而不是实现的接口行为。这一点和 C# 对实现接口方法的要求是一致的:实现接口成员必须声明为public

4.2. 接着看 Java 的不同

转到 Java 这边,情况就不同了,编译根本不让过

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    default void walk() {
        out.println("Bird.walk()");
    }
}

// Duplicate default methods named walk with the parameters () and ()
// are inherited from the types Bird and Person
class BirdPerson implements Person, Bird { }

这个意思就是,PersonBird都为签名相同的walk方法定义了默认现,所以编译器不知道BirdPerson到底该怎么办了。那么如果只有一个walk有默认实现呢?

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    void walk();
}

// The default method walk() inherited from Person conflicts
// with another method inherited from Bird
class BirdPerson implements Person, Bird { }

这意思是,两个接口行为不一致,编译器还是不知道该怎么处理BirdPerson

总之,不管怎么样,就是要BirdPerson必须实现自己的walk()。既然BirdPerson自己实现了walk(),那调用行为也就没有什么悬念了:

BirdPerson birdPerson = new BirdPerson();
birdPerson.walk();              // 输出 BirdPerson.walk()
((Person) birdPerson).walk();   // 输出 BirdPerson.walk()
((Bird) birdPerson).walk();     // 输出 BirdPerson.walk()

4.3. 结论,多继承没有问题

如果一个类实现的多个接口中定义了相同签名的方法,没有默认实现的情况下,当然不会有问题。

如果类中实现了这个签名的方法,那无论如何,调用的都是这个方法,也不会有问题。

但在接口有默认实现,而类中没有实现的情况下,C# 将实际行为交给引用类型去处理;Java 则直接报错,交给开发者去处理。笔者比较赞同 C# 的做法,毕竟默认方法的初衷就是为了不强制开发者去处理增加接口方法带来的麻烦。

5. 问题三,更复杂的情况怎么去分析

对于更复杂的情况,多数时候还是可以猜到会怎么去调用的,毕竟有个基本原则在那里。

5.1. 在类中的实现优先

比如,WalkBase定义了Walk()方法,但没实现任何接口,BirdPersonWalkBase继承,实现了IPerson接口,但没实现Walk()方法,那么该执行哪个Walk呢?

会执行WalkBase.Walk()——不管什么情况下,类方法优先

class WalkBase
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson { }

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 输出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 输出 WalkBase.Walk()
}

如果父类子类都有实现,但子类不是“重载”,而是“覆盖”实现,那要根据引用类型来找最近的类,比如

class WalkBase : IBird  // <== 注意这里实现了 IBird
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson  // <== 这里_没有_实现 IBird
{
    // 注意:这里是 new,而不是 override
    public new void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 输出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 输出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 输出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 输出 WalkBase.Walk()
}

如果WalkBase中以virtual定义Walk(),而BirdPerson中以override定义Walk(),那毫无悬念输出全都是BirdPerson.Walk()

class WalkBase : IBird
{
    public virtual void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson
{
    public override void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 输出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 输出 BirdPerson.Walk()
    ((IPerson)birdPerson).Walk();   // 输出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 输出 BirdPerson.Walk()
}

上面示例中的候最后一句输出,是通过IBird.Walk()找到WalkBase.Walk(),而WalkBase.Walk()又通过虚方法链找到BirdPerson.Walk(),所以输出仍然是BirdPerson.Walk()。学过 C++ 的同学这时候可能就会很有感觉了!

至于 Java,所有方法都是虚方法。虽然可以通过final让它非虚,但是在子类中不能定义相同签名的方法,所以 Java 的情况会更简单一些。

5.2. 类中无实现,根据引用类型找最近的默认实现

还是拿WalkBaseBirdPerson分别实现了IBirdIPerson的例子,

class WalkBase : IBird { }
class BirdPerson : WalkBase, IPerson { }

((IPerson)birdPerson).Walk();   // 输出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 输出 IBird.Walk()

哦,当然 Java 中不存在,因为编译器会要求必须实现BirdPerson.Walk()

以上就是C#中接口默认方法的详细内容了,看完之后是否有所收获呢?如果想了解更多相关内容,欢迎关注创新互联网站制作公司行业资讯!

创新互联www.cdcxhl.cn,专业提供香港、美国云服务器,动态BGP最优骨干路由自动选择,持续稳定高效的网络助力业务部署。公司持有工信部办法的idc、isp许可证, 机房独有T级流量清洗系统配攻击溯源,准确进行流量调度,确保服务器高可用性。佳节活动现已开启,新人活动云服务器买多久送多久。


当前名称:C#中的接口默认方法是什么?-创新互联
文章起源:http://hbruida.cn/article/dhjipe.html