小编典典

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

c#

我们都知道,可变结构通常是邪恶的。我也很确定,因为IEnumerable<T>.GetEnumerator()返回类型IEnumerator<T>,所以结构会立即装箱成引用类型,这比起它们仅仅是引用类型的成本要高。

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

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


阅读 349

收藏
2020-05-19

共1个答案

小编典典

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

您问为什么这不会导致拳击。这是因为C#编译器不会避免将代码填充到foreach循环中的IEnumerable或IEnumerator中的东西!

当我们看到

foreach(X x in c)

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

这具有两个理想的效果。

首先,如果该集合是int的集合,但是在发明泛型类型之前编写的,则它不会对将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是一个只读字段,则编译器将执行其操作:创建一个副本,并对副本进行突变,以确保该方法不会丢弃需要处置的值。

但是,这与我们对应该在此处发生的事情的直觉相冲突:

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

我们希望在using块内执行MoveNext 会将 枚举数移动到下一个枚举数,而不管它是struct类型还是ref类型。

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

  • 如果通过方法进行变异的值型变量是普通局部变量,则通常进行变异

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

不幸的是,该规范在此问题上提供的指导很少。显然,由于我们的操作不一致,导致某些问题已损坏,但是 正确 的做法尚不清楚。

2020-05-19