为什么通用接口默认情况下不支持协变和逆变?

21
例如 IEnumerable<T> 接口:
public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}
在这个接口中,泛型类型仅用作接口方法的返回类型,并且不用作方法参数的类型,因此它可以是协变的。基于这一点,编译器是否可以从接口中推断出方差?如果可以,为什么C#要求我们显式设置协变/逆变关键字?
更新:正如Jon Skeet所提到的,这个问题可以分成以下子问题:
1.编译器是否可以通过当前泛型类型及其所有基类型内部使用情况推断泛型类型的协变/逆变性? 例如..在.NET Framework 4.0中有多少个泛型接口参数可以自动标记为协变/逆变而不会产生歧义?约70%、80%、90%还是100%?
2.如果可以的话,编译器是否应该默认应用泛型类型的协变/逆变性?至少对于那些它能够分析并推断出类型使用中的协变/逆变性的类型。

2
很好的问题。当我们设计这个功能时,我在2007年就预料到了你的问题。这就是为什么我当时写了一篇文章来回答它:http://blogs.msdn.com/b/ericlippert/archive/2007/10/29/covariance-and-contravariance-in-c-part-seven-why-do-we-need-a-syntax-at-all.aspx - Eric Lippert
3
以下有关为什么这不是自动的一些优秀答案。我只想补充一点,如果您接受ReSharper的建议,它将建议共变/逆变,并为您进行重构。但我不知道它在模糊/困难情况下的效果如何(我认为在这些情况下它将不会尝试提供建议)。 - Stephen Cleary
2个回答

16
首先,这里有两个问题。第一,编译器总是能做到吗?第二,如果可以的话,它应该这样做吗?
对于第一个问题,我会引用Eric Lippert的评论。在《C#深入》第二版中,当我提出这个问题时,他发表了以下评论:
“即使我们想要这样做,我也不确定我们是否能够合理地做到这一点。我们很容易遇到需要对程序中所有接口进行昂贵的全局分析才能确定差异的情况,并且我们很容易遇到只有或的情况,无法在它们之间做出决定。由于性能不佳和模糊不清的情况,这是一个不太可能的功能。”
(我希望Eric不介意我引用他的原话;他以前非常擅长分享这些见解,所以我根据过去的表现来判断:)
另一方面,我怀疑还有一些情况可以毫无歧义地推断出来,因此第二个问题仍然相关......
我认为即使编译器可以明确知道它是有效的,也不应该自动进行。扩展接口通常是某种程度上的破坏性变化,但如果你是唯一实现它的人,那么通常不会是破坏性变化。然而,如果人们依赖于你的接口来进行变异,那么你可能无法添加方法而不破坏客户端......即使他们只是调用者,而不是实现者。您添加的方法可能会将先前协变的接口更改为不变的接口,此时您会破坏任何试图以协变方式使用它的调用者。
基本上,我认为需要明确要求这一点——这是一个您应该有意识地做出的设计决策,而不是仅仅在没有考虑过协变/逆变的情况下意外地得到协变/逆变。

1
如果将泛型类型的协变/逆变性引入可能会导致“破坏客户端”,那么为什么.NET创建者在.NET 4.0中将其主要的泛型接口替换为协变/逆变性对应物呢? - Konstantin Tarkus
3
将不变类型变为可变类型实际上是一种破坏性改变;我们认为这种改变的好处值得这种打破。请参阅我的文章:http://blogs.msdn.com/b/ericlippert/archive/2007/11/02/covariance-and-contravariance-in-c-part-nine-breaking-changes.aspx - 然而,Jon正确地指出,他所说的那种破坏是指编辑接口可能会意外地改变其法定差异注释。因此,我们希望将这些注释明确化,而不是推断它们。 - Eric Lippert
2
@Jon:关于“我希望Eric不介意……”- 请引用我的话。我相信你会在正确的语境下引用我的话。 :-) - Eric Lippert
1
你能想到任何客户端依赖于通用接口类型不变的示例吗?如果将通用接口从不变更为可变是一种破坏性的变化,那么受此变化影响的代码百分比可能是多少?换句话说,百万开发人员中有多少人会抱怨这个问题? :) - Konstantin Tarkus
1
如果允许代码确定对象无法强制转换为给定类型,则有可能编写的代码在以前不允许的强制转换变为允许时会出现错误。同样,如果允许代码确定某个类不支持某些方法,则有可能编写的代码在以前缺少该方法的类中添加该方法时会出现错误。 - supercat
显示剩余3条评论

5
这篇文章解释了编译器无法推断的情况,并提供了明确的语法:

本文解释了编译器无法推断的情况,并提供了明确的语法:

interface IReadWriteBase<T>
{
    IReadWrite<T> ReadWrite();
}

interface IReadWrite<T> : IReadWriteBase<T>
{

}

这里的“in”和“out”,都可以使用吗?你是如何推断的?

编译器可以根据使用情况默认在基础接口上应用 out 关键字,对于派生接口也是如此,基于父类型参数 T。 - Konstantin Tarkus
是的,你可以手动设置两个,但从编译器的角度来看,在你的例子中它可以默认将T设置为协变。是或否? - Konstantin Tarkus
是的,但如果编译器做出了这个决定,而我想使用 in,在这种情况下我们会有一个冲突,只能通过明确指出来解决。 - Darin Dimitrov
如果你想指定 in,编译器会按照你的指定保留它,而不会尝试推断这个类型。如果你没有明确指定 in/out,编译器会自行做出假设。 - Konstantin Tarkus
2
我通常不喜欢编译器代表我做出假设...如果存在可能的歧义,编译器绝不能做出那种假设。你如何知道你的接口是协变还是逆变? - Thomas Levesque
2
@Thomas:确实如此。更糟糕的是,如果其他接口继承自此接口或在其方法的形式参数或返回类型中具有此接口,则对它们的分析取决于此接口是协变还是逆变。选择其中一种方式会产生不局限的影响。 - Eric Lippert

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