为什么IEnumerable <t>在C#4中变成了协变?</t>

时间:2011-07-18 11:36:28

标签: c# generics ienumerable language-design covariance

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

public interface IEnumerable<T> : IEnumerable

由于C#4的定义是:

public interface IEnumerable<out T> : IEnumerable
  • 是否只是让LINQ表达式中令人讨厌的强制转换消失?
  • 这不会在C#中引入与string[] <: object[](破碎数组差异)相同的问题吗?
  • 从兼容性的角度来看,如何完成协方差的添加?早期代码是否仍适用于更高版本的.NET,或者是否需要重新编译?反过来怎么样?
  • 以前使用此界面的代码在所有情况下都是严格不变的,或者某些用例现在可能会有不同的行为吗?

3 个答案:

答案 0 :(得分:47)

Marc和CodeInChaos的答案非常好,但只是添加了一些细节:

首先,听起来您有兴趣了解我们为完成此功能而进行的设计过程。如果是这样,那么我鼓励您阅读我在设计和实现该功能时撰写的冗长系列文章。从底部开始:

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

  

是否只是让LINQ表达式中令人讨厌的强制转换消失?

不,它不是只是来避免Cast<T>表达式,但这样做是激励我们做这个功能的激励因素之一。我们意识到“为什么我不能在这种采用一系列动物的方法中使用一系列长颈鹿?”的数量会有所增加。问题,因为LINQ鼓励使用序列类型。我们知道我们想首先将协方差添加到IEnumerable<T>

我们实际上考虑过甚至在C#3中制作IEnumerable<T>协变,但是如果不引入任何人使用的整个功能,那么这样做会很奇怪。

  

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

它不直接引入该问题,因为编译器仅在已知类型安全时才允许方差。但是,它确实保留破坏的数组方差问题。使用协方差,IEnumerable<string[]>可隐式转换为IEnumerable<object[]>,因此如果您有一系列字符串数组,您可以将其视为一系列对象数组,然后您遇到与以前相同的问题:您可以尝试将Giraffe放入该字符串数组中并在运行时获得异常。

  

从兼容性的角度来看,如何完成协方差的添加?

小心。

  

早期的代码是否仍适用于更高版本的.NET,或者是否需要重新编译?

只有一种方法可以找到答案。尝试一下,看看失败了!

如果X!= Y,尝试强制针对.NET X编译的代码针对.NET Y运行通常是一个坏主意,无论类型系统如何变化。

  

另一种方式呢?

答案相同。

  

某些用例现在可能会有不同的行为吗?

绝对。使接口协变在以前不变的地方在技术上是一个“突破性变化”,因为它可能导致工作代码中断。例如:

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

IE<T>不协变时,此代码选择ABC或DEF,或两者都不选。当它是协变的时候,它再也不会选择DEF了。

或者:

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

之前,如果您在D的实例上调用了M并且以一系列海龟作为参数,则重载决策选择B.M,因为这是唯一适用的方法。如果IE是协变的,那么重载解析现在选择DM,因为两种方法都适用,并且更多派生类的适用方法总是胜过较少派生类的适用方法,无论参数类型匹配是否准确。

或者:

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是不变的,那么对d.M(weird)的调用将解析为B.M.如果IE突然变为协变,那么两个方法都适用,两者都比基类上的方法更好,并且两者都不比另一个更好,因此,重载决策变得模糊,我们报告错误。

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

答案 1 :(得分:28)

按顺序:

  

是否只是让LINQ表达式中令人讨厌的强制转换消失?

它使人们的行为就像人们通常期望的那样; p

  

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

没有;因为它没有暴露任何Add机制或类似机制(并且不能; outin在编译器中强制实施)

  

从兼容性的角度来看,如何完成协方差的添加?

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

  

早期的代码是否仍适用于更高版本的.NET,或者是否需要重新编译?

它完全向后兼容,但是依赖 C#4.0方差的C#将无法在C#2.0等编译器中编译

  

另一种方式呢?

这不是不合理的

  

以前使用此接口的代码在所有情况下都是严格不变的,或者某些用例现在可能会有不同的行为吗?

某些BCL通话(IsAssignableFrom)现在可能会有不同的回复

答案 2 :(得分:9)

  

是否只是让LINQ表达式中令人讨厌的强制转换消失?

不仅在使用LINQ时。只要您拥有IEnumerable<Derived>并且代码需要IEnumerable<Base>

,它就会很有用
  

这不会在C#中引入与string[]&lt ;: object[](破碎数组差异)相同的问题吗?

不,因为协方差仅允许返回该类型值的接口,但不接受它们。所以这很安全。

  

从兼容性的角度来看,如何完成协方差的添加?早期代码是否仍适用于更高版本的.NET,或者是否需要重新编译?反过来呢?

我认为已编译的代码大部分都是按原样运行的。某些运行时类型检查(isIsAssignableFrom,...)将在之前返回false的位置返回true。

  

以前使用此接口的代码在所有情况下都是严格不变的,或者某些用例现在可能会有不同的行为吗?

不确定你的意思


最大的问题与重载解决有关。由于现在可能会进行额外的隐式转换,因此可能会选择不同的重载。

void DoSomething(IEnumerabe<Base> bla);
void DoSomething(object blub);

IEnumerable<Derived> values = ...;
DoSomething(values);

但是,当然,如果这些重载行为不同,那么API的设计已经很糟糕了。