为啥 BCL 集合使用结构枚举器,而不是类?

Posted

技术标签:

【中文标题】为啥 BCL 集合使用结构枚举器,而不是类?【英文标题】:Why do BCL Collections use struct enumerators, not classes?为什么 BCL 集合使用结构枚举器,而不是类? 【发布时间】:2011-03-11 05:53:14 【问题描述】:

我们都知道mutable structs are evil。我也很确定,因为IEnumerable<T>.GetEnumerator() 返回类型IEnumerator<T>,结构会立即被装箱到引用类型中,这比它们开始时只是引用类型的成本更高。

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

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

【问题讨论】:

其他人在此运行的相关页面:***.com/questions/384511/…eggheadcafe.com/software/aspnet/31702392/… 【参考方案1】:

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

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

当我们看到

foreach(X x in c)

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

这有两个理想的效果。

首先,如果集合是一个整数的集合,但是是在泛型类型被发明之前编写的,那么它不会受到将 Current 的值装箱到对象然后将其拆箱到 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();
    ...

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

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

如果通过方法改变的值类型变量是正常的局部变量,那么它会正常改变

但如果它是一个提升的局部变量(因为它是一个匿名函数的封闭变量或在一个迭代器块中),那么局部 实际上是作为只读字段生成的,并且确保在副本上发生突变的设备接管。

不幸的是,规范在这方面几乎没有提供任何指导。很明显,有些东西出了问题,因为我们的做法前后不一致,但正确该做的事情根本不清楚。

【讨论】:

+1 这意味着传递 IEnumerable<T> 与原始通用集合相比存在(最小)性能损失——在快速发布模式测试中枚举 List<int> 1000 万个条目,无论是直接还是投射到 IEnumerable<int>,我都看到了 2:1 的一致时间差异(在这种情况下,~100ms vs ~50ms)。 很好的答案,我不知道那个优化 - 但它非常有意义。我确实觉得有点讽刺的是,我链接了你的博客来支持我关于可变结构是邪恶的声明——你回答了我的问题 :) 我想知道为什么这个答案如此灵通;然后我看到了是谁写的。 但是现在呢?情况是否有所好转? @Backwards_Dave:没错。编译器不是要求调用GetEnumerator;需要生成枚举集合的代码。如果它可以在不调用GetEnumerator 的情况下这样做,因为数组是一种非常特殊的类型,其行为是 100% 已知的,那么它可以选择这样做。【参考方案2】:

结构体的方法在编译时知道结构体类型时是内联的,通过接口调用方法很慢,所以答案是:因为性能原因。

【讨论】:

但是这些是内部结构,所以在编译时永远不知道类型;并且所有最终用户代码都通过接口访问它们。 如果您查看例如 List.GetEnunmerator 方法(msdn.microsoft.com/en-us/library/b0yss765.aspx),您可以看到它返回 List::Enumerator 结构。 C#中的foreach循环不直接使用IEnumerable接口,如果类有GetEnumerator方法就足够了。所以枚举器的类型在编译时是已知的。 +1,这是准确的。 JIT 编译器可以生成更高效的代码。

以上是关于为啥 BCL 集合使用结构枚举器,而不是类?的主要内容,如果未能解决你的问题,请参考以下文章

枚举器而不是类实例化的构造函数?

为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?

为啥 BCL 中没有 AutoResetEventSlim?

为啥使用常量而不是枚举?

Java 枚举 - 为啥使用 toString 而不是 name

结构体,枚举类型