【问题标题】:Interface conflict resolution in C#C# 中的接口冲突解决方案
【发布时间】:2019-07-25 02:42:52
【问题描述】:

这是一个基于Eric Lippert's answer on this question 的衍生问题。

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

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&lt;Turtle&gt; 的实现视为默认实现并要求对两者进行特定转换来解决。

检索枚举器就像一个魅力:

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);

【问题讨论】:

  • 如果您想知道为什么 C# 是这样设计的,请询问语言设计人员。没有人能告诉你为什么这门语言是这样设计的。
  • 征求人们对如何设计语言的意见主要是基于意见的,在本网站上不是一个合适的问题。
  • @Servy:我强烈同意“为什么不”的问题通常含糊不清并寻求意见,但这个问题说得很清楚。原始发布者希望对特定决策有具体的技术利弊因素;这是一个可以回答的问题。
  • 我认为不选择另一种语言是一种很好的方式来告诉你:“你做错了”。没有什么“可以”是长颈鹿列表和海龟列表。但是,可以拥有这些列表,就像在这个问题中发布的情况一样
  • @EricLippert 当然。征求意见的问题是可以回答的。征求意见的问题不在于无法提供答案,而在于有太多可能的答案,因为每个人都可以对他们认为应该是什么有自己的看法。

标签: c# linq oop interface enumerator


【解决方案1】:

首先要了解使用什么机制来解决对扩展方法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&lt;Giraffe&gt;
  • 实现IEnumerable&lt;Turtle&gt;
  • IEnumerable&lt;T&gt; 扩展 IEnumerable 并且是协变的。
  • TurtleGiraffe 扩展 Animal 扩展 object

现在,如果您知道唯一件事,并且您知道我们正在寻找IEnumerable&lt;A&gt;,那么您可以对A 得出什么结论?

有多种可能性:

  • 选择Animal,或object
  • 在决胜局中选择TurtleGiraffe
  • 判断情况不明确,并给出错误。

我们可以拒绝第一个选项。 C# 的一个设计原则是:当面临选项之间的选择时,总是选择其中一个选项,否则会产生错误。 C# 从不说“你让我在AppleCake 之间做出选择,所以我选择Food”。它总是从你给它的选择中选择,或者它说它没有做出选择的基础。

此外,如果我们选择Animal,只会让情况变得更糟。请参阅本文末尾的练习。

您提出了第二个选项,您提出的决胜局是“隐式实现的接口优先于显式实现的接口”。

这个提议的 tiebreaker 有一些问题,从 开始没有隐式实现的接口。让我们把你的情况稍微复杂一点:

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()) 时会发生什么?这两个接口都不是“明确实现的”。这两个接口都不是“隐式实现的”。 接口成员是隐式或显式实现的,而不是接口

现在我们可以说“一个所有成员都隐式实现的接口是一个隐式实现的接口”。这有帮助吗?没有。因为在您的示例中,IEnumerable&lt;Turtle&gt; 具有一个隐式实现的成员和一个显式实现的成员:返回 IEnumeratorGetEnumerator 的重载是 IEnumerable&lt;Turtle&gt; 的成员,并且您已经显式实现了它.

(旁白:一位评论者指出,上述措辞不雅;从规范中“继承”自“基”接口的成员是否是“派生”接口的“成员”,或者仅仅是接口之间的“派生”关系只是声明“派生”接口的任何实现者也必须实现“基”的要求。规范在这一点上一直不清楚,可以提出论据不管怎样,我的观点是派生接口要求你实现一组特定的成员,其中一些成员可以隐式实现,一些可以显式实现,我们可以计算如何我们应该选择很多。)

所以现在可能提议的决胜局是“计算成员数,显式实现的成员最少的接口是赢家”。

所以让我们退后一步,问一个问题:您到底会如何记录此功能?您将如何解释它?假设一位顾客来找您说“为什么在这里选择乌龟而不是长颈鹿?”你会怎么解释?

现在假设客户问“我如何才能预测编译器在我编写代码时会做什么?”请记住,该客户可能没有Ark 的源代码;它可能是第三方库中的一种类型。 您的提案将第三方对用户不可见的实施决策变成了控制他人代码正确与否的相关因素。开发人员通常反对使他们无法理解其代码的功能的功能,除非有相应的功能提升。

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

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

更糟糕的是,它不会导致编译错误;想象这样一种情况,有人只是在隐式实现的方法的数量上进行了更改,而这些方法甚至不是您调用的方法,但这种更改会默默地导致一个序列的乌龟变成一个序列的长颈鹿。

那些场景真的非常糟糕。 C# 被精心设计以防止这种“脆弱的基类”失败。

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

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

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

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

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

或者,从实现类Ark 的人的角度考虑它。如果该人希望表达意图“这种类型可以用作乌龟和长颈鹿的序列,但如果有歧义,请选择乌龟”。您是否相信任何希望表达这种信念的开发人员会自然而轻松地得出这样的结论:这样做的方法是使接口之一更隐式地实现比别人好吗?

如果这是开发人员需要能够消除歧义的事情,那么应该有一个设计良好、清晰、可发现的具有这些语义的功能。比如:

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

例如。也就是说,该特征应该是显而易见的可搜索的,而不是从关于该类型的公共表面积应该是什么的不相关决定中偶然出现的。

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

最后,我把最重要的原因留到了最后。你说:

我不考虑以这种方式设计课程是否被认为是最佳实践的反馈。

但这是一个极其重要的因素! C# 的规则并非旨在对糟糕的代码做出正确的决定。它们旨在将糟糕的代码变成无法编译的损坏代码,而这种情况已经发生。系统有效!

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

(可以肯定地指出,错误消息在诊断问题方面做得很差;这会导致另外一大堆微妙的设计决策。我打算针对这些场景改进该错误消息,但这些场景是太罕见了,无法将它们列为优先事项,而且我在 2012 年离开 Microsoft 之前并没有做到这一点。显然,在随后的几年里,也没有其他人将其列为优先事项。)


更新:您问为什么调用ark.GetEnumerator 可以自动执行正确的操作。这是一个容易得多的问题。这里的原理很简单:

重载解决方案选择可访问适用的最佳成员。

“可访问”表示调用者可以访问该成员,因为它“足够公开”,“适用”表示“所有参数都匹配其形参类型”。

当您致电ark.GetEnumerator() 时,问题是不是“我应该选择IEnumerable&lt;T&gt; 的哪个实现”?这根本不是问题。问题是“哪个GetEnumerator() 既可访问又适用?”

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


练习:将ark 转换为IEnumerable&lt;Animal&gt; 会发生什么?做出预测:

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

现在试试你的预测,看看会发生什么。得出关于编写具有相同泛型接口的多种构造的类型是好还是坏的结论。

【讨论】:

  • “因为你不应该这样做,所以 C# 编译器团队没有动力花一分钟来弄清楚如何帮助你做得更好。”这也许可以总结一下,句号。感谢您澄清有关通用类型推断算法的一两件事。
  • 现在我也更清楚了,重载解析泛型类型推断背后有不同的原因。演习表明,糟糕的设计应该受到惩罚。我相信我的所有问题都已得到解答。
  • 关于“返回IEnumeratorGetEnumerator的重载是IEnumerable&lt;Turtle&gt;的成员”。 AFAIK即使一个接口“继承”另一个接口,“基”接口的成员也不被视为“派生”接口的成员——它们可以通过“派生”接口调用,但不能通过它显式实现。不确定 C#8 默认接口成员,但很可能适用相同的原则。
  • @IvanStoev:这是一个很好的观点;规范在这一点上一直不清楚,我早就发现这很烦人。我会澄清文本。
猜你喜欢
  • 2014-05-20
  • 1970-01-01
  • 2017-12-18
  • 1970-01-01
  • 1970-01-01
  • 2013-01-13
  • 1970-01-01
  • 2011-02-12
  • 1970-01-01
相关资源
最近更新 更多