我们都知道 可变结构是邪恶的 一般来说。我也很确定,因为 IEnumerable<T>.GetEnumerator() 返回类型 IEnumerator<T>, ,这些结构立即被装箱为参考类型,其成本要比仅仅是参考类型开始。

那么,为什么在BCL通用集合中,所有可枚举者都可以变形结构呢?当然必须有充分的理由。我唯一发生的事情是可以轻松复制结构,从而在任意点保存枚举状态。但是添加一个 Copy() 方法 IEnumerator 界面本来会减少麻烦,因此我认为这本身就是合乎逻辑的理由。

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

有帮助吗?

解决方案

确实,出于绩效原因。 BCL团队做了一个 很多 在决定使用您正确地称为可疑和危险的做法之前,有关这一点的研究:使用可变的价值类型。

您问为什么这不会引起拳击。这是因为C#编译器不会生成代码以在foreach循环中为Ienumerable或Ienumerator打包物,如果可以避免它!

当我们看到的时候

foreach(X x in c)

我们要做的第一件事是检查C是否具有称为GetEnumerator的方法。如果是这样,那么我们检查一下它返回的类型是否具有moveNext和属性当前。如果确实如此,则使用对这些方法和属性的直接调用,完全生成foreach循环。只有当“模式”无法匹配的情况下,我们才能回到寻找界面。

这有两个理想的效果。

首先,如果该集合是一个INT的集合,但是在发明通用类型之前写的,则不会对将电流的值拳击给对象进行拳击罚款,然后将其拆箱。如果电流是返回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();
    ...
}

我们希望在使用块内进行movenext 将要 无论是结构类型还是REF类型,将枚举者移至下一个枚举者。

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

  • 如果通过方法突变的值变量是正常局部性,则正常突变

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

不幸的是,该规格对此事几乎没有指导。显然,某些事情被打破了,因为我们不一致,但是 要做的事情根本不清楚。

其他提示

当在编译时间中知道结构类型时,结构方法是内衬的,并且通过接口调用方法很慢,因此答案是:由于性能原因。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top