这是一个基于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
的三个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);
答案 0 :(得分:13)
重要的是,首先要了解正在使用什么机制来解决对扩展方法Select
的调用。 C#使用通用类型推断算法,该算法相当复杂;有关详细信息,请参见C#规范。 (我真的应该写一篇博客文章来解释这一切;我在2006年录制了一个有关它的视频,但不幸的是它已经消失了。)
但基本上,Select上的泛型类型推断的思想是:我们有:
public static IEnumerable<R> Select<A, R>(
this IEnumerable<A> items,
Func<A, R> projection)
通话中
ark.Select(x => x)
我们必须推断出A
和R
的意图。
由于R
依赖于A
,并且实际上等于A
,因此问题减少到找到A
。我们仅有的信息是ark
的 type 。我们知道ark
:
Ark
object
IEnumerable<Giraffe>
IEnumerable<Turtle>
IEnumerable<T>
扩展了IEnumerable
并且是协变的。Turtle
和Giraffe
扩展了Animal
,扩展了object
。现在,如果这些只是您知道的,并且您知道我们正在寻找IEnumerable<A>
,那么关于A
可以得出什么结论?>
有很多可能性:
Animal
或object
。Turtle
或Giraffe
。我们可以拒绝第一种选择。 C#的设计原理是:面对选项之间的选择时,请始终选择其中一个选项,否则会产生错误。 C#从未说过“您给了我Apple
和Cake
之间的选择,所以我选择了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();}
}
当我们致电C.DoIt(new C())
时会发生什么?这两个接口均未“明确实现”。两个接口都不是“隐式实现的”。 接口成员是隐式或显式实现的,而不是接口。
现在我们可以说“所有成员都隐式实现的接口是隐式实现的接口”。有帮助吗?不。因为在您的示例中,IEnumerable<Turtle>
有一个隐式实现的成员和一个显式实现的成员:返回GetEnumerator
的{{1}}的重载是IEnumerator
的成员,而您已经明确实现了。
所以现在提议的决胜局可能是“计算成员数,而明确实现成员最少的接口是获胜者”。
因此,让我们退后一步,问一个问题:您将如何记录此功能?您将如何解释?假设有一位顾客来找您,并说:“为什么在这里选择乌龟而不是长颈鹿?”您将如何解释?
现在假设客户问:“我如何预测编写代码时编译器将执行的操作?”请记住,客户可能没有IEnumerable<Turtle>
的源代码。它可能是第三方库中的一种。 您的建议将第三方对用户的不可见实施决策变成控制其他人的代码是否正确的相关因素。开发人员通常会反对使他们无法理解其代码功能的功能,除非相应地增强了功能。
(例如:虚方法使您无法知道代码的作用,但是它们非常有用;没有人认为这种提议的功能具有类似的有用性。)
假设第三方更改了一个库,以便以您依赖的类型显式实现不同数量的成员。现在会发生什么? 第三方更改是否明确实现成员可能导致他人代码中的编译错误。
更糟的是,它可能不会导致编译错误;想象一下这样一种情况,即某人仅在隐式实现的方法数量上进行更改,而这些方法甚至都不是您调用的方法,而是默默地导致更改一系列的乌龟变成了一系列的长颈鹿。
这些情况真的非常糟糕。 C#是经过精心设计的,可以防止此类“脆性基类”失败。
哦,但是情况变得更糟。假设我们确实喜欢这个决胜局。我们甚至可以可靠地实现它吗?
我们怎么知道成员是否被明确实现?程序集中的元数据具有一个表,该表列出了哪些类成员显式映射到了哪些接口成员,但是这是C#源代码中的内容的可靠反映吗??
不,不是!在某些情况下,C#编译器必须秘密地代表您生成显式实现的接口,以使验证者满意(描述它们将完全不在主题之列)。因此,您实际上不能很容易地分辨出类型的实现者决定显式实现多少个接口成员。
情况变得更糟:假设该类甚至没有在C#中实现?某些语言总是填充在显式接口表中,实际上,我认为Visual Basic可能是其中的一种。因此,您的建议是使VB中编写的类的类型推断规则与C#中编写的等效类型可能不同。
尝试向刚刚将类从VB移植到C#的人解释,以使其具有相同的公共接口,现在他们的测试停止编译。
或者,从实现类Ark
的人员的角度考虑它。如果该人希望表达这种意图,“这种类型既可以用作海龟也可以用作长颈鹿,但是如果有歧义,请选择海龟”。您是否相信任何希望表达这种信念的开发人员都会自然而轻松地得出这样的结论:做到这一点的方法是使其中一个接口更加隐式地实现比另一个?
如果这是开发人员需要消除歧义的事情,那么应该有一个设计合理,清晰,可发现的功能,具有这些语义。像这样:
Ark
例如。也就是说,该功能应该显而易见和可搜索,而不是偶然地从有关该类型的公共表面区域的无关决定中浮现出来。
简而言之:明确实现的接口成员的数量不是.NET类型系统的一部分。这是一个私有的实施策略决策,而不是编译器应该用来制定决策的公开场合。
最后,我把最重要的原因留在了最后。你说:
我不是在寻求反馈意见,以这种方式设计课程是否被视为最佳实践。
但这是一个非常重要的因素! C#的规则并非旨在就糟糕的代码做出良好的决策;他们的目的是将code脚的代码变成无法编译的残破代码,并且这种情况已经发生。该系统有效!
使一个类实现同一泛型接口的两个不同版本是一个很糟糕的主意,您不应该这样做。 因为您不应该这样做,所以C#编译器团队没有动力花一点时间弄清楚如何帮助您做得更好。此代码给您一条错误消息。 很好。这应该!该错误消息告诉您您做错了,所以请别再做错了,然后再正确地做。如果这样做时很痛,请停止这样做!
(可以肯定地指出,错误消息在诊断问题上做得很差;这导致了另一堆微妙的设计决策。我打算针对这些情况改进该错误消息,但是这些情况是太罕见了,因此无法将其作为优先事项,而且我在2012年离开微软之前就没有提到它。显然,在随后的几年中,没有其他人将其列为优先事项。)
更新:您问为什么调用class Ark : default IEnumerable<Turtle>, IEnumerable<Giraffe> ...
可以自动执行正确的操作。这是一个容易得多的问题。这里的原理很简单:
重载分辨率选择可同时访问 和适用的最佳成员。
“可访问”表示调用者有权访问该成员,因为它“足够公开”,而“适用”表示“所有参数都与它们的形式参数类型匹配”。
当您致电ark.GetEnumerator
时,问题不是 “我应该选择ark.GetEnumerator()
的哪个实现”?这根本不是问题。问题是“哪个IEnumerable<T>
既可访问又适用?”
只有一个,因为明确实现的接口成员不是GetEnumerator()
的可访问成员。只有一个可访问的成员,并且恰好适用。 Cem重载解析的明智规则之一是如果只有一个可访问的适用成员,请选择它!
锻炼:将Ark
投射到ark
会发生什么?做出预测:
现在尝试您的预测,看看会发生什么。得出结论,编写具有相同泛型接口的多个结构的类型是好是坏。