为什么 IEnumerable在 C# 4 中进行协变?

发布于 2024-11-24 05:38:09 字数 484 浏览 1 评论 0原文

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

public interface IEnumerable<T> : IEnumerable

从 C# 4 开始,定义是:

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

In earlier versions of C# IEnumerable was defined like this:

public interface IEnumerable<T> : IEnumerable

Since C# 4 the definition is:

public interface IEnumerable<out T> : IEnumerable
  • Is it just to make the annoying casts in LINQ expressions go away?
  • Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?
  • How was the addition of the covariance done from a compatibility point of view? Will earlier code still work on later versions of .NET or is recompilation necessary here? What about the other way around?
  • Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

旧伤慢歌 2024-12-01 05:38:10

为了:

这只是为了让 LINQ 表达式中烦人的强制转换消失吗?

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

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

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

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

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

早期代码是否仍可在更高版本的 .NET 上运行,或者是否需要重新编译?

然而,它完全向后兼容:依赖 C# 4.0 变体的 C# 无法在 C# 2.0 等编译器中编译

反过来呢?

这并非没有道理

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

某些 BCL 调用 (IsAssignableFrom) 可能返回不同的结果现在

In order:

Is it just to make the annoying casts in LINQ expressions go away?

It makes things behave like people generally expect ;p

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

No; since it doesn't expose any Add mechanism or similar (and can't; out and in are enforced at the compiler)

How was the addition of the covariance done from a compatibility point of view?

The CLI already supported it, this merely makes C# (and some of the existing BCL methods) aware of it

Will earlier code still work on later versions of .NET or is recompilation necessary here?

It is entirely backwards compatible, however: C# that relies on C# 4.0 variance won't compile in a C# 2.0 etc compiler

What about the other way around?

That is not unreasonable

Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?

Some BCL calls (IsAssignableFrom) may return differently now

阿楠 2024-12-01 05:38:10

这只是为了让 LINQ 表达式中烦人的强制转换消失吗?

不仅在使用 LINQ 时。只要有 IEnumerable 并且代码需要 IEnumerable,它就很有用。

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

不可以,因为只有返回该类型值但不接受它们的接口才允许协变。所以很安全。

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

我认为已经编译的代码大部分都会按原样工作。某些运行时类型检查(isIsAssignableFrom...)将在之前返回 false 的地方返回 true。

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

不确定你的意思


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

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

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

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

Is it just to make the annoying casts in LINQ expressions go away?

Not only when using LINQ. It's useful everywhere you have an IEnumerable<Derived> and the code expects a IEnumerable<Base>.

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

No, because covariance is only allowed on interfaces that return values of that type, but don't accept them. So it's safe.

How was the addition of the covariance done from a compatibility point of view? Will earlier code still work on later versions of .NET or is recompilation necessary here? What about the other way around?

I think already compiled code will mostly work as is. Some runtime type-checks (is, IsAssignableFrom, ...) will return true where they returned false earlier.

Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?

Not sure what you mean by that


The biggest problems are related to overload resolution. Since now additional implicit conversions are possible a different overload might be chosen.

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

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

But of course, if these overload behave differently, the API is already badly designed.

雪若未夕 2024-12-01 05:38:09

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

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

协变和逆变博客文章

这只是为了让 LINQ 表达式中烦人的强制转换消失吗?

不,这不仅仅是为了避免 Cast 表达式,而是这样做是鼓励我们实现此功能的动机之一。我们意识到,“为什么我不能在这种采用动物序列的方法中使用长颈鹿序列?”的数量将会增加。问题,因为 LINQ 鼓励使用序列类型。我们知道我们首先要向 IEnumerable 添加协方差。

我们实际上考虑过在 C# 3 中使 IEnumerable 协变,但认为如果不引入整个功能供任何人使用,这样做会很奇怪。

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

它不会直接引入该问题,因为编译器仅在已知类型安全时才允许变化。然而,它确实保留了损坏的数组方差问题。通过协方差,IEnumerable 可以隐式转换为 IEnumerable,因此如果您有一个字符串数组序列,则可以将其视为作为对象数组的序列,然后您会遇到与以前相同的问题:您可以尝试将长颈鹿放入该字符串数组中并在运行时得到异常。

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

小心。

早期代码是否仍可在更高版本的 .NET 上运行,或者是否需要重新编译?

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

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

反过来呢?

同样的答案。

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

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

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

IE 不是协变时,此代码选择 ABC 或 DEF 或两者都不选择。当它是协变时,它不再选择 DEF。

或者:

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

之前,如果您以海龟序列作为参数对 D 的实例调用 M,重载决策会选择 BM,因为这是唯一适用的方法。如果 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 是不变的,那么对 dM(weird) 的调用解析为 BM 如果 IE 突然变得协变,那么 DM 的两种方法都适用,两者都比基类上的方法更好,而且都不是比另一个更好,因此,重载解析变得不明确,我们报告错误。

当我们决定进行这些重大更改时,我们希望(1)这种情况很少见,并且(2)当出现这种情况时,几乎总是因为该类的作者正在尝试模拟语言中的协方差那个没有。通过直接添加协方差,希望当代码在重新编译时“中断”时,作者可以简单地删除试图模拟现有功能的疯狂装备。

Marc's and CodeInChaos's answers are pretty good, but just to add a few more details:

First off, it sounds like you are interested in learning about the design process we went through to make this feature. If so, then I encourage you to read my lengthy series of articles that I wrote while designing and implementing the feature. Start from the bottom of the page:

Covariance and contravariance blog posts

Is it just to make the annoying casts in LINQ expressions go away?

No, it is not just to avoid Cast<T> expressions, but doing so was one of the motivators that encouraged us to do this feature. We realized that there would be an uptick in the number of "why can't I use a sequence of Giraffes in this method that takes a sequence of Animals?" questions, because LINQ encourages the use of sequence types. We knew that we wanted to add covariance to IEnumerable<T> first.

We actually considered making IEnumerable<T> covariant even in C# 3 but decided that it would be strange to do so without introducing the whole feature for anyone to use.

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

It does not directly introduce that problem because the compiler only allows variance when it is known to be typesafe. However, it does preserve the broken array variance problem. With covariance, IEnumerable<string[]> is implicitly convertible to IEnumerable<object[]>, so if you have a sequence of string arrays, you can treat that as a sequence of object arrays, and then you have the same problem as before: you can try to put a Giraffe into that string array and get an exception at runtime.

How was the addition of the covariance done from a compatibility point of view?

Carefully.

Will earlier code still work on later versions of .NET or is recompilation necessary here?

Only one way to find out. Try it and see what fails!

It's often a bad idea to try to force code compiled against .NET X to run against .NET Y if X != Y, regardless of changes to the type system.

What about the other way around?

Same answer.

Is it possible that certain use cases will behave different now?

Absolutely. Making an interface covariant where it was invariant before is technically a "breaking change" because it can cause working code to break. For example:

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

When IE<T> is not covariant, this code chooses either ABC or DEF or neither. When it is covariant, it never chooses DEF anymore.

Or:

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

Before, if you called M on an instance of D with a sequence of turtles as the argument, overload resolution chooses B.M because that is the only applicable method. If IE is covariant, then overload resolution now chooses D.M because both methods are applicable, and an applicable method on a more-derived class always beats an applicable method on a less-derived class, regardless of whether the argument type match is exact or not.

Or:

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) {}
}

If IE is invariant then a call to d.M(weird) resolves to B.M. If IE suddenly becomes covariant then both methods D.M are applicable, both are better than the method on the base class, and neither is better than the other, so, overload resolution becomes ambiguous and we report an error.

When we decided to make these breaking changes, we were hoping that (1) the situations would be rare, and (2) when situations like this arise, almost always it is because the author of the class is attempting to simulate covariance in a language that doesn't have it. By adding covariance directly, hopefully when the code "breaks" on recompilation, the author can simply remove the crazy gear trying to simulate a feature that now exists.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文