为什么BCL Collections使用struct枚举器而不是类?

时间:2010-07-02 18:43:12

标签: c# .net ienumerator base-class-library

我们都知道mutable structs are evil。我也非常确定,因为IEnumerable<T>.GetEnumerator()返回类型IEnumerator<T>,结构会立即被装入引用类型,比它们只是简单的引用类型要花费更多。

那么为什么在BCL泛型集合中,所有枚举器都是可变的结构?当然必须有一个很好的理由。我唯一想到的就是可以轻松复制结构,从而在任意点保留枚举器状态。但是在Copy()接口中添加IEnumerator方法本来就不那么麻烦了,所以我不认为这本身就是一种逻辑上的理由。

即使我不同意设计决定,我也希望能够理解它背后的原因。

2 个答案:

答案 0 :(得分:75)

确实,这是出于性能原因。 BCL团队在决定采用你正确称之为可疑和危险的做法之前,对这一点进行了大量的研究:使用可变值类型。

你问为什么这不会导致拳击。这是因为C#编译器在foreach循环中没有生成代码来将内容包装到IEnumerable或IEnumerator中,如果它可以避免它的话!

当我们看到

foreach(X x in c)

我们要做的第一件事是检查c是否有一个名为GetEnumerator的方法。如果是,那么我们检查它返回的类型是否有方法MoveNext和属性current。如果是,则完全使用对这些方法和属性的直接调用生成foreach循环。只有当“模式”无法匹配时,我们才会回头寻找接口。

这有两个理想的效果。

首先,如果集合是,例如,一组int,但是在发明泛型类型之前编写,那么它不会将装箱值的装箱惩罚用于对象,然后将其拆箱为int。如果Current是一个返回int的属性,我们只是使用它。

其次,如果枚举器是值类型,那么它不会将枚举器打包到IEnumerator。

就像我说的那样,BCL团队对此进行了大量研究,并发现在绝大多数情况下,分配和解除分配枚举器的代价足够大,值得制作它是一种值类型,即使这样做会导致一些疯狂的错误。

例如,考虑一下:

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h = somethingElse;
}

你会非常正确地期望尝试改变h失败,事实确实如此。编译器检测到您正在尝试更改具有待处置的某些内容的值,并且这样做可能会导致需要处理的对象实际上不被处置。

现在假设你有:

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h.Mutate();
}

这里发生了什么?如果h是只读字段,您可能会合理地期望编译器会执行它所做的事情:make a copy, and mutate the copy以确保该方法不会丢弃需要处理的值中的内容。

然而,这与我们对这里应该发生的事情的直觉相冲突:

using (Enumerator enumtor = whatever)
{
    ...
    enumtor.MoveNext();
    ...
}

我们希望在使用块中执行MoveNext将将枚举数移动到下一个,无论它是结构还是ref类型。

不幸的是,C#编译器今天有一个bug。如果您处于这种情况,我们会选择不一致的策略。今天的行为是:

  • 如果通过方法变异的值类型变量是正常的局部变量,则它会正常变异

  • 但如果它是一个悬挂的本地(因为它是匿名函数或迭代器块中的一个封闭变量),那么本地实际上是生成为只读字段,以及确保在副本上发生突变的装备接管。

不幸的是,该规范对此事提供的指导很少。显然有些东西被打破了,因为我们做的不一致,但正确要做的事情一点也不清楚。

答案 1 :(得分:6)

在编译时已知struct类型的结构方法,并且通过接口调用方法很慢,因此回答是:因为性能原因。