为啥 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以上是关于为啥 BCL 集合使用结构枚举器,而不是类?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?
为啥 BCL 中没有 AutoResetEventSlim?