C# 8.0默认接口成员的意外行为

20

考虑以下代码:

interface I {
    string M1() => "I.M1";
    string M2() => "I.M2";
}

abstract class A : I {}

class C : A {
    public string M1() => "C.M1";
    public virtual string M2() => "C.M2";
}

class Program {
    static void Main() {
        I obj = new C();
        System.Console.WriteLine(obj.M1());
        System.Console.WriteLine(obj.M2());
    }
}

在.NET Core 3.1.402中,它会产生以下意外输出:

I.M1
C.M2

A 没有实现接口成员 I 的隐式或显式实现,因此我预期默认实现将用于 C,因为 C 继承了 A 的接口映射并且没有显式重新实现 I。根据 ECMA-334 (18.6.6) 和 C# 6.0 语言规范:

一个类继承其基类提供的所有接口实现。

未显式 重新实现 接口的情况下,派生类无法以任何方式更改它从基类继承的接口映射。

特别是,我预期以下输出:

I.M1
I.M2

如果没有将A声明为抽象类,就会发生这种情况。

在C# 8.0中,上面的代码行为是有意的吗,还是某个bug的结果?如果是有意的,为什么只有当M2被声明为虚拟的(对于M1不是)并且A被声明为抽象时,C中的方法才隐式实现相应的I成员?

编辑:

虽然我仍然不确定这是一个bug还是一个feature(我倾向于认为这是一个bug,而第一条评论中链接的讨论至今没有得出结论),但我想到了一个更加危险的场景:

class Library {
    private interface I {
        string Method() => "Library.I.Method";
    }
    
    public abstract class A: I {
        public string OtherMethod() => ((I)this).Method();
    }
}

class Program {
    private class C: Library.A {
        public virtual string Method() => "Program.C.Method";
    }
    
    static void Main() {
        C obj = new C();
        System.Console.WriteLine(obj.OtherMethod());
    }
}

注意接口 Library.I 和类 Program.C 分别对它们所在的类是私有的。特别地,方法 Program.C.Method 应当无法从类 Program 外部访问。 类 Program 的作者可能认为完全掌控了何时调用方法 Program.C.Method,甚至可能不知道接口 Library.I(因为它是私有的)。然而,Library.A.OtherMethod 中会调用该方法,输出如下:

Program.C.Method

这看起来像是一种脆弱基类的问题。 Program.C.Method 声明为公共的事实应该是无关紧要的。请参阅 Eric Lippert 的这篇博客文章,其中描述了一个不同但有些相似的情况。


1
在GitHub上提问:https://github.com/dotnet/csharplang/issues/52#issuecomment-706745987 - aepot
1
在 dotnet/runtime 中创建了一个 bug。链接 - aepot
1
@aepot 感谢您进行更多的调查并提交了一个合适的错误报告。虽然我不了解运行时内部,但我确实倾向于认为这是一个错误 - 相当严重的错误。我在我的帖子中添加了一个新的场景来解释我的观点。 - Bartosz
1个回答

4
自C# 8.0引入接口的默认实现以来,支持对接口实现成员的查找过程进行更改。关键部分在于如何定义实例(在您的示例中为obj)或类型语法。
让我们从执行成员解析的7.3种方式开始,并使用C obj = new C();替换I obj = new C();。当运行此代码时,将打印以下输出: C.M1 C.M2 正如您所看到的,两个WriteLine的结果都打印出类C定义的实现。这是因为类型语法引用了一个类,并且“最先使用”的实现是类C的实现。
现在,当我们将代码改回I obj = new C();时,我们会看到不同的结果,即: I.M1 C.M2 这是因为虚拟和抽象成员不会被最具体的实现替换,就像M1(未标记为virtual)那样。
现在,仍然存在一个主要问题,即为什么只有在将方法声明为虚拟(在M2的情况下而不是M1),并且只有在A声明为抽象时,C中的方法才会隐式实现I的相应成员?
当A类是非抽象类时,它正在“主动”实现接口,而当它是抽象类时,该类仅要求继承抽象类的类也实现接口。 当我们看你的例子时,我们无法编写这个:|
A obj = new C(); System.Console.WriteLine(obj.M1()); // Method M1() is not defined
要了解更多信息,请查看此处:https://github.com/dotnet/roslyn/blob/master/docs/features/DefaultInterfaceImplementation.md 这里有一些变化及其结果:
I obj = new C(); // 以A为抽象类 结果为 I.M1 C.M2
I obj = new C(); // 以A为类 结果为 I.M1 I.M2
C obj = new C(); // 使用或不使用A作为抽象类 结果为 C.M1 C.M2

I obj = new A(); // with A as class 的结果是 I.M1 I.M2


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接