2010-07-02 61 views
43

一般我们都知道mutable structs are evil。我也很确定,因为IEnumerable<T>.GetEnumerator()返回类型IEnumerator<T>,结构立即被装箱成一个引用类型,比如果它们仅仅是引用类型开始,花费更多。为什么BCL集合使用结构枚举器而不是类?

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

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

+0

别人过这个运行相关的页面: http://stackoverflow.com/questions/384511/enumerator-implementation-use-struct-or-class http://www.eggheadcafe.com/software/ aspnet/31702392/c-compiler-challenge - s.aspx – 2010-07-02 18:59:08

回答

62

事实上,这是出于性能原因。 BCL团队在决定采用一种可疑和危险的做法来决定采用什么样的方式进行研究之前做了批次的研究:使用可变值类型。

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

当我们看到

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; 
} 

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

现在,假设你有:

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

这里会发生什么?如果h是一个只读字段,您可能会合理地认为编译器会执行它的操作:make a copy, and mutate the copy为了确保该方法不会丢弃需要处理的值中的东西。

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

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

我们希望做一个使用块内的MoveNext将移动枚举到下一个一个struct或一个ref类型。

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

  • 如果值类型变量被通过的方法突变是一种正常的地方,然后它通常突变

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

不幸的是,该规范很少提供这方面的指导。显然有些事情因为我们做得不一致而被打破,但是要做的事情一点都不清楚。

+1

+1这意味着有身边掠过的(最小的)性能损失的'IEnumerable的',而不是原来的泛型集合 - 在快速释放模式测试列举了一个'名单'千万条目都直接并且当投射到“IEnumerable ”时,我看到2:1的一致时间差(在这种情况下,〜100ms vs〜50ms)。 – 2010-07-02 19:09:04

+0

好的答案,我不知道这种优化 - 但它非常有意义。我觉得有些讽刺的是,我挂你的博客备份我的声明可变的结构是邪恶的 - 你回答我的问题:) – Eloff 2010-07-02 23:14:42

+0

顺便说一句,这是一个丑陋的极端情况,但与可变结构的另一个问题。 – Eloff 2010-07-02 23:32:50

5

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

+0

但是这些都是内部结构,所以这种类型在编译时是不知道的;所有最终用户代码都通过接口访问它们。 – 2010-07-02 18:53:49

+1

@Stephen:'List .Enumerator'被记录为在MSDN中公开... – 2010-07-02 19:04:40

+0

如果你看例如列表 .GetEnunmerator方法(http://msdn.microsoft.com/en-us/library/b0yss765 .aspx)你可以看到它返回列表 ::枚举器结构。 C#中的foreach循环不直接使用IEnumerable接口,如果类具有GetEnumerator方法,则足够了。所以枚举类型在编译时是已知的。 – STO 2010-07-02 19:05:19