C#中的接口冲突解决

5 c# linq oop interface enumerator

这是根据埃里克·利珀特(Eric Lippert)对这个问题的回答得出的一个附带问题

我想知道为什么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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,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
Run Code Online (Sandbox Code Playgroud)

为什么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);
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 19

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

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

public static IEnumerable<R> Select<A, R>(
  this IEnumerable<A> items,
  Func<A, R> projection)
Run Code Online (Sandbox Code Playgroud)

从通话中

ark.Select(x => x)
Run Code Online (Sandbox Code Playgroud)

我们必须推断出什么AR意图。

由于R取决于A,实际上等于A,问题减少了寻找A。我们拥有的唯一信息是类型ark。我们知道ark

  • Ark
  • 延伸 object
  • 机具 IEnumerable<Giraffe>
  • 机具 IEnumerable<Turtle>
  • IEnumerable<T>扩展IEnumerable并且是协变的。
  • TurtleGiraffe延伸Animal延伸object

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

有多种可能性:

  • 选择Animalobject
  • 选择TurtleGiraffe通过决胜局。
  • 确定情况不明确,并给出错误。

我们可以拒绝第一种选择。C#的设计原理是:面对选项之间的选择时,请始终选择其中一个选项,否则会产生错误。C#从不说“您给了我一个选择AppleCake所以我选择了Food”。它总是从您给出的选择中进行选择,或者说它没有做出选择的依据。

而且,如果我们选择Animal,那只会使情况变得更糟。请参阅本文结尾处的练习。

您提出第二种选择,而您提出的决胜局是“隐式实现的接口比显式实现的接口具有优先权”。

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

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();}
}
Run Code Online (Sandbox Code Playgroud)

当我们打电话C.DoIt(new C())时会发生什么?这两个接口均未“明确实现”。两个接口都不是“隐式实现的”。 接口成员是隐式或显式实现的,而不是interface

现在我们可以说“所有成员都隐式实现的接口是隐式实现的接口”。有帮助吗?不。因为在您的示例中,IEnumerable<Turtle>隐式实现了一个成员,而显式实现了一个成员:GetEnumerator返回的重载IEnumerator是的成员,IEnumerable<Turtle>而您已经显式实现了它

(旁白:一个评论者指出,以上措词不恰当;从规范中还不能完全清楚地知道,从“基本”接口“继承”的成员是否是“派生”接口的“成员”,或者仅仅是接口之间的“派生”关系只是要求“派生的”接口的任何实现者也必须实现“基数”的要求的陈述,这一点在历史上尚不清楚,因此可以以任何一种方式进行论证。无论如何,我的观点是,派生接口需要您实现一组特定的成员,并且其中一些成员可以隐式实现,而某些则可以显式实现,我们可以算出应该选择的数量。)

因此,现在提议的决胜局可能是“计算成员人数,并且明确实现最少成员的接口是获胜者”。

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

现在假设客户问:“我如何预测编写代码时编译器将执行的操作?” 请记住,客户可能没有源代码Ark;它可能是第三方库中的一种。 您的建议使第三方对用户不可见的实施决策成为控制其他人代码是否正确的相关因素。开发人员通常会反对使他们无法理解其代码功能的功能,除非相应地增强了功能。

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

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

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

这些情况确实非常糟糕。C#是经过精心设计的,可以防止此类“脆性基类”失败。

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

我们什至怎么知道一个成员是否被明确实现?程序集中的元数据具有一个表,该表列出了哪些类成员已显式映射到哪些接口成员,但这是对C#源代码中内容的可靠反映吗?

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

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

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

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

如果这是开发人员需要消除歧义的事情,那么应该有一个设计合理,清晰,可发现的功能以及这些语义。就像是:

class Ark : default IEnumerable<Turtle>, IEnumerable<Giraffe> ...
Run Code Online (Sandbox Code Playgroud)

例如。也就是说,该功能应该是显而易见的可搜索的,而不是由于对该类型的公共表面积应该是什么的无关决定而偶然出现。

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

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

我不是在寻求反馈意见,以这种方式设计课程是否被视为最佳实践。

但这是一个极其重要的因素!C#的规则并不是为了对糟糕的代码做出正确的决定;他们的目的是将糟糕的代码变成无法编译的残破代码,并且这种情况已经发生。该系统有效!

创建一个实现同一泛型接口的两个不同版本的类是一个可怕的想法,您不应该这样做。 因为您不应该这样做,所以C#编译器团队没有动力花一点时间弄清楚如何帮助您做得更好。此代码给您一条错误消息。那很好。这应该!该错误消息告诉您您做错了,因此请停止做错并开始做对。如果这样做时很痛,请停止这样做!

(肯定可以指出,错误消息在诊断问题上做得很差;这导致了另外一堆微妙的设计决策。我打算针对这些情况来改进该错误消息,但是这种情况很少见,使其成为当务之急,而我在2012年离开微软之前就没有去做。显然,在随后的几年中,没有其他人将其作为优先事项。)


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

重载分辨率选择可访问适用的最佳成员。

“可访问”是指调用者有权访问该成员,因为它“足够公开”,而“可应用”是指“所有参数都匹配其形式参数类型”。

当您打电话时ark.GetEnumerator(),问题不是IEnumerable<T>我应该选择哪种实现”?这根本不是问题。问题是“哪些GetEnumerator()既可访问又适用?”

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


练习:当你施放会发生什么arkIEnumerable<Animal>?做出预测:

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

现在尝试您的预测,看看会发生什么。得出结论,编写具有相同通用接口的多个结构的类型是好是坏。

  • 现在,对我而言,更清楚的是_overload resolution_和_generic type inference_背后有不同的推理。练习表明不良的设计应受到惩罚。我相信我所有的问题都已经回答。 (5认同)