为什么IEnumerable <T>在C#4中是协变的?

在早期版本的C# IEnumerable中定义如下:

 public interface IEnumerable<T> : IEnumerable 

由于C#4的定义是:

 public interface IEnumerable<out T> : IEnumerable 
  • 只是为了让LINQexpression式中的恼人的表演消失吗?
  • 这不会像C#中的string[] <: object[] (破坏数组方差)一样引入相同的问题吗?
  • 从兼容性的angular度来看,如何增加协变? 早期的代码仍然可以在更高版本的.NET上工作,或者在这里重新编译吗? 那反过来呢?
  • 之前的代码是否使用这个接口在所有情况下都是严格不变的,或者现在某些用例可能会有所不同?

Marc和CodeInChaos的答案相当不错,但只是增加了一些细节:

首先,这听起来像您有兴趣了解我们devise过程中所做的function。 如果是这样,那么我鼓励你阅读我在devise和实现这个function时写的冗长的一系列文章。 从底部开始:

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx

只是为了让LINQexpression式中的恼人的表演消失吗?

不,这不仅仅是为了避免Cast<T>expression,而是鼓励我们做这个function的动机之一。 我们意识到,为什么我不能用这个方法中的一系列动物来使用一系列长颈鹿呢? 问题,因为LINQ鼓励使用序列types。 我们知道我们想要首先向IEnumerable<T>添加协方差。

我们实际上已经考虑过甚至在C#3 IEnumerable<T>协变,但是决定如果不引入任何人使用的全部特性,这将是奇怪的。

这不会像C#中的string[] <: object[] (破坏数组方差)一样引入相同的问题吗?

它不直接引入这个问题,因为编译器只有在已知types安全的情况下才允许有差异。 但是,它确实保留了破坏的数组方差问题。 使用协方差, IEnumerable<string[]>可隐式转换为IEnumerable<object[]> ,因此如果有一串string数组,则可以将其视为一系列对象数组,然后就像以前一样:你可以尝试把一个长颈鹿放到这个string数组中,并在运行时得到一个exception。

从兼容性的angular度来看,如何增加协变?

小心。

早期的代码仍然可以在更高版本的.NET上工作,或者在这里重新编译吗?

只有一个办法找出来。 尝试一下,看看有什么失败!

如果X!= Y,试图强制针对.NET X编译的代码针对.NET Y运行,而不考虑types系统的更改通常是一个糟糕的主意。

那反过来呢?

同样的答案。

某些用例现在可能会有所不同吗?

绝对。 在以前不变的界面协变下,在技术上是一个“突变”,因为它可能导致工作代码被破坏。 例如:

 if (x is IEnumerable<Animal>) ABC(); else if (x is IEnumerable<Turtle>) DEF(); 

IE<T>不是协变时,这个代码selectABC或DEF或者不select。 当它是协变时,它不再selectDEF。

要么:

 class B { public void M(IEnumerable<Turtle> turtles){} } class D : B { public void M(IEnumerable<Animal> animals){} } 

之前,如果您以D序列的海龟作为参数对M的实例进行调用,则重载决策selectBM,因为这是唯一适用的方法。 如果IE是协变的,那么重载parsing现在selectDM,因为这两种方法都是适用的,并且对于更派生类的适用方法总是在较less派生类上击败适用的方法,而不pipe参数types匹配是否准确。

要么:

 class Weird : IEnumerable<Turtle>, IEnumerable<Banana> { ... } class B { public void M(IEnumerable<Banana> bananas) {} } class D : B { public void M(IEnumerable<Animal> animals) {} public void M(IEnumerable<Fruit> fruits) {} } 

如果IE是不变的,那么调用dM(weird)parsing为BM。如果IE突然变成协变,那么两种方法DM都适用,都比基类上的方法好,而且都不比其他更好,所以,重载parsing变得模棱两可,我们报告一个错误。

当我们决定做出这些突破性的改变时,我们希望(1)情况是罕见的,(2)当这种情况出现时,几乎总是因为类的作者试图模仿语言的协变没有它。 通过直接添加协方差,希望当代码在重新编译时“中断”时,作者可以简单地删除试图模拟现在存在的特征的疯狂装置。

为了:

只是为了让LINQexpression式中的恼人的表演消失吗?

它使事情像人们普遍期望的那样行事;

这不会像C#中的string[] <: object[] (破坏数组方差)一样引入相同的问题吗?

没有; 因为它不公开任何Add机制或类似的(而且不能;在编译器中强制执行)

从兼容性的angular度来看,如何增加协变?

CLI已经支持它,这只会让C#(以及一些现有的BCL方法)意识到它

早期的代码仍然可以在更高版本的.NET上工作,或者在这里重新编译吗?

但它完全向后兼容: 依赖于C#4.0差异的C#不会在C#2.0等编译器中编译

那反过来呢?

这不是不合理的

之前的代码是否使用这个接口在所有情况下都是严格不变的,或者现在某些用例可能会有所不同?

一些BCL调用( IsAssignableFrom )现在可能会有所不同

只是为了让LINQexpression式中的恼人的表演消失吗?

不仅在使用LINQ时。 在有IEnumerable<Derived>任何地方都是有用的,代码需要一个IEnumerable<Base>

这不会像C#中的string[] <: object[] (破坏数组方差)一样引入相同的问题吗?

不,因为只有在返回该types值的接口上才允许协方差,但不接受它们。 所以这是安全的。

从兼容性的angular度来看,如何增加协变? 早期的代码仍然可以在更高版本的.NET上工作,或者在这里重新编译吗? 那反过来呢?

我认为已编译的代码将主要按原样工作。 一些运行时types检查( IsAssignableFrom ,…)将在之前返回false的情况下返回true。

之前的代码是否使用这个接口在所有情况下都是严格不变的,或者现在某些用例可能会有所不同?

不知道你是什么意思


最大的问题是与超载分辨率有关。 由于现在可以进行额外的隐式转换,因此可以select不同的过载。

 void DoSomething(IEnumerabe<Base> bla); void DoSomething(object blub); IEnumerable<Derived> values = ...; DoSomething(values); 

但是,当然,如果这些超载的行为不同,API已经devise得不好。