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

我们都知道可变结构总体上是邪恶的。 我也很确定,因为IEnumerable<T>.GetEnumerator()返回IEnumerator<T>types,所以结构立即被装箱到一个引用types中,比起它们只是引用types开销更多。

那么为什么在BCLgenerics集合中,所有的枚举器都是可变结构呢? 当然必须有一个很好的理由。 唯一发生在我身上的是可以很容易地复制结构体,从而将枚举器状态保留在任意点上。 但是,向IEnumerator接口添加一个Copy()方法本来就不那么麻烦,所以我不认为这是一个合理的理由。

即使我不同意devise决定,我也希望能够理解背后的理由。

事实上,这是出于性能的原因。 BCL团队在决定采用一种可疑的危险做法的时候,对这个问题做了大量的研究:使用可变值types。

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

当我们看到

 foreach(X x in c) 

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

这有两个理想的效果。

首先,如果集合是一个整数集合,但是在genericstypes被发明之前写入,那么它不会承担拳击Current对象的值的装箱惩罚,然后将其拆箱为int。 如果Current是一个返回int的属性,我们就使用它。

其次,如果枚举器是一个值types,那么它不会将枚举器装箱到IEnumerator。

就像我说的,BCL团队在这方面做了大量的研究,发现绝大多数时候,分配和解除分配人员的惩罚足够大,值得把它作为一种价值types,尽pipe这样做可以导致一些疯狂的错误。

例如,考虑一下:

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

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

现在假设你有:

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

这里发生了什么? 如果h是一个只读字段,你可能会合理地认为编译器会这样做: 创build一个副本,并改变副本 ,以确保该方法不会丢弃需要处理的值的东西。

然而,这与我们直觉有什么应该发生在这里的冲突:

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

我们希望在一个using块中做一个MoveNext 会将枚举器移动到下一个,无论它是一个struct还是一个reftypes。

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

  • 如果通过方法突变的值typesvariables是正常本地,那么它正常地被突变

  • 但是如果它是一个悬挂的本地(因为它是匿名函数的封闭variables或迭代器块),那么本地实际上是作为只读字段生成的,并且确保在副本上发生突变的装置需要过度。

不幸的是,这个规范在这个问题上没有提供任 显然有些事情因为我们做的不一致而被打破了,但是正确的做法并不完全清楚。

在编译时struct结构的types是已知的,并且通过接口调用方法很慢,所以struct的方法是内联的,所以答案是:由于性能原因。