C#中的接口冲突解决

Interface conflict resolution in C#

这是一个基于 的衍生问题。

我想知道为什么C#语言被设计成在以下特定情况下无法检测到正确的接口成员。我不关注以这种方式设计 class 是否被认为是最佳实践的反馈。

class Turtle { }
class Giraffe { }

class Ark : IEnumerable<Turtle>, IEnumerable<Giraffe>
{
    public IEnumerator<Turtle> GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable.GetEnumerator'
    IEnumerator IEnumerable.GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable<Giraffe>.GetEnumerator'
    IEnumerator<Giraffe> IEnumerable<Giraffe>.GetEnumerator()
    {
        yield break;
    }
}

在上面的代码中,Ark 有 3 个与 GetEnumerator() 相互冲突的实现。此冲突通过将 IEnumerator<Turtle> 的实现视为默认实现并要求对其他两者进行特定转换来解决。

检索枚举器非常有效:

var ark = new Ark();

var e1 = ((IEnumerable<Turtle>)ark).GetEnumerator();  // turtle
var e2 = ((IEnumerable<Giraffe>)ark).GetEnumerator(); // giraffe
var e3 = ((IEnumerable)ark).GetEnumerator();          // object

// since IEnumerable<Turtle> is the default implementation, we don't need
// a specific cast to be able to get its enumerator
var e4 = ark.GetEnumerator();                         // turtle

为什么 LINQ 的 Select 扩展方法没有类似的解决方案?是否有适当的设计决定允许解决前者而不是后者之间的不一致?

 // This is not allowed, but I don't see any reason why ..
 // ark.Select(x => x);                                // turtle expected

 // these are allowed
 ark.Select<Turtle, Turtle>(x => x);
 ark.Select<Giraffe, Giraffe>(x => x);

首先了解使用什么机制来解析对扩展方法的调用很重要 Select。 C# 使用 通用类型推断 算法,该算法相当复杂;有关详细信息,请参阅 C# 规范。 (我真的应该写一篇博客文章来解释这一切;我在 2006 年录制了一段关于它的视频,但不幸的是它已经消失了。)

但基本上,Select 上的泛型类型推断的想法是:我们有:

public static IEnumerable<R> Select<A, R>(
  this IEnumerable<A> items,
  Func<A, R> projection)

来自来电

ark.Select(x => x)

我们必须推断出 AR 的意图。

由于 R 依赖于 A,并且实际上等于 A,因此问题简化为寻找 A。我们拥有的唯一信息是 ark 类型 。我们知道 ark:

  • Ark
  • 扩展 object
  • 实施IEnumerable<Giraffe>
  • 实施IEnumerable<Turtle>
  • IEnumerable<T> 扩展 IEnumerable 并且是协变的。
  • TurtleGiraffe 扩展 Animal 扩展 object.

现在,如果这些是您所知道的唯一,并且您知道我们正在寻找IEnumerable<A>,您可以得出什么结论A?

有多种可能性:

  • 选择Animal,或object
  • 通过决胜局选择TurtleGiraffe
  • 判定情况不明确,报错。

我们可以拒绝第一个选项。 C#的一个设计原则是:当面临选项之间的选择时,总是选择其中一个选项,否则会产生错误。 C# 从不说 "you gave me a choice between Apple and Cake so I choose Food"。它总是从你给它的选择中选择,或者它说它没有做出选择的基础。

此外,如果我们选择Animal,那只会让情况变得更糟。请参阅此 post.

末尾的练习

您提出第二个选项,您提出的决胜局是"an implicitly implemented interface gets priority over an explicitly implemented interface"。

这个提议的决胜局有一些问题,首先是没有隐式实现的接口。让我们让你的情况稍微复杂一点:

interface I<T>
{
  void M();
  void N();
}
class C : I<Turtle>, I<Giraffe>
{
  void I<Turtle>.M() {} 
  public M() {} // Used for I<Giraffe>.M
  void I<Giraffe>.N() {}
  public N() {}
  public static DoIt<T>(I<T> i) {i.M(); i.N();}
}

当我们调用 C.DoIt(new C()) 时会发生什么?这两个接口都不是 "explicitly implemented"。这两个接口都不是 "implicitly implemented"。 接口成员是隐式或显式实现的,而不是接口

现在我们可以说 "an interface that has all of its members implicitly implemented is an implicitly implemented interface"。这有帮助吗?没有。因为在您的示例中,IEnumerable<Turtle> 隐式实现了一个成员,显式实现了一个成员: GetEnumerator 的重载 returns IEnumeratorIEnumerable<Turtle> 并且您已明确实施它

(旁白:一位评论者指出,上述措辞不雅;从规范中还不完全清楚来自 "base" 接口的成员 "inherited" 是否是 "members" "derived" 接口,或者仅仅是接口之间的 "derivation" 关系仅仅是对 "derived" 接口的任何实现者也必须实现 "base" 的要求的陈述。规范在历史上一直不清楚这一点,并且可以以任何一种方式进行论证。无论如何,我的观点是派生接口需要你实现一组特定的成员,和一些这些成员中的一部分可以隐式实现,有些可以显式实现,我们可以计算我们应该选择的每个成员有多少。)

所以现在提议的决胜局可能是 "count the members, and the interface that has the least members explicitly implemented is the winner"。

那么让我们退后一步并提出以下问题:您究竟如何记录此功能?您将如何解释它?假设一位客户来找您并说 "why are turtles being chosen over giraffes here?" 您会如何解释?

现在假设客户问 "how can I make a prediction about what the compiler will do when I write the code?" 请记住,客户可能没有 Ark 的源代码;它可能是 third-party 库中的一种类型。 你的提案将第三方的invisible-to-users实现决策变成了控制他人代码正确与否的相关因素。开发人员通常反对使他们无法理解代码功能的功能,除非相应的功能得到提升。

(例如:虚拟方法让人无法知道您的代码做了什么,但它们非常有用;没有人认为这个提议的功能具有类似的有用性奖励。)

假设第三方更改了一个库,以便在您所依赖的类型中显式实现了不同数量的成员。现在发生了什么? 第三方更改成员是否显式实现可能会导致其他人的代码出现编译错误

更糟糕的是,它不会导致编译错误;想象这样一种情况,某人 仅在隐式实现的方法数量 上进行了更改,而这些方法甚至都不是方法这就是你 调用 ,但这种变化会悄无声息地导致一系列乌龟变成一系列长颈鹿。

那些场景真的非常糟糕。 C# 经过精心设计以防止此类 "brittle base class" 故障。

哦,但情况变得更糟了。假设我们确实喜欢这个决胜局;我们甚至可以可靠地实施它吗?

我们如何判断一个成员是否被显式实现?程序集中的元数据有一个 table 列出了哪些 class 成员显式映射到哪些接口成员,但是 是 C# 源代码中内容的可靠反映?

不,不是!在某些情况下,C# 编译器必须 秘密地 代表您生成显式实现的接口,以满足验证者的要求(描述它们会很离题)。所以你实际上不能很容易地告诉类型的实现者决定显式实现多少接口成员。

更糟的是:假设 class 甚至没有在 C# 中实现?有些语言 总是 填写显式接口 table,事实上我认为 Visual Basic 可能是这些语言之一。因此,您的建议是使在 VB 中编写的 classes 的类型推断规则 可能与在 C# 中编写的等效类型不同

尝试向刚刚将 class 从 VB 移植到 C# 以具有相同的 public 接口的人解释一下,现在他们的测试停止 编译.

或者,从实施者的角度考虑classArk。如果那个人想表达这个意图"this type can be used as both a sequence of turtles and giraffes, but if there is an ambiguity, choose turtles"。您是否相信任何希望表达这种信念的开发人员都会 自然而轻松地 得出这样的结论,那就是使其中一个接口 更隐含实施 比其他?

如果那是开发人员需要能够消除歧义的那种东西,那么应该有一个 well-designed、清晰、可发现的具有这些语义的特征。类似于:

class Ark : default IEnumerable<Turtle>, IEnumerable<Giraffe> ...

例如。也就是说,该特征应该是 明显的可搜索的,而不是从关于 public 表面积的无关决定中偶然出现的类型应该是。

简而言之:显式实现的接口成员数不是 .NET 类型系统的一部分。这是一个私有实现策略决策,而不是编译器应该用来做出决策的 public 表面。

最后,我把最重要的原因放在最后。你说:

I am not looking on feedback whether designing a class this way is considered best practice.

但这是一个极其重要的因素! C# 的规则并不是为了对蹩脚的代码做出正确的决定而设计的;它们旨在将蹩脚的代码变成无法编译的损坏代码,而这已经发生了。系统有效!

制作一个实现同一通用接口的两个不同版本的 class 是一个糟糕的主意,您不应该这样做。 因为你不应该这样做,所以 C# 编译器团队没有动力花一分钟的时间来弄清楚如何帮助你做得更好。此代码为您提供错误消息。 很好。这应该!该错误消息告诉您您做错了,所以停止做错并开始做对。如果你这样做的时候很痛,就停止这样做!

(当然可以指出,错误消息在诊断问题方面做得很差;这导致了另一大堆微妙的设计决策。我打算针对这些情况改进该错误消息,但是场景太少了,无法将它们设为高优先级,我在 2012 年离开 Microsoft 之前没有考虑到它。显然,在接下来的几年里,也没有其他人将它作为优先级。)


更新:你问为什么调用 ark.GetEnumerator 可以自动做正确的事情。这是一个更容易的问题。这里的原理很简单:

过载解析选择既可访问适用的最佳成员。

"Accessible"表示调用者可以访问该成员,因为是"public enough","applicable"表示"all the arguments match their formal parameter types".

当您调用 ark.GetEnumerator() 时,问题是 不是 "which implementation of IEnumerable<T> should I choose"?这根本不是问题。问题是"which GetEnumerator() is both accessible and applicable?"

只有一个,因为显式实现的接口成员是Ark的不可访问成员。可访问成员只有一个,恰好适用。 C# 重载决议的明智规则之一是如果只有一个可访问的适用成员,请选择它!


练习:将 ark 转换为 IEnumerable<Animal> 会发生什么?做出预测:

  • 我会得到一个海龟序列
  • 我会得到一系列长颈鹿
  • 我会得到一系列长颈鹿和海龟
  • 我会得到一个编译错误
  • 我会得到别的东西——什么?

现在试试你的预测,看看到底发生了什么。得出关于编写具有同一通用接口的多个构造的类型是好还是坏的主意的结论。