为什么BCL收集使用结构枚举者,而不是类?
-
02-10-2019 - |
题
我们都知道 可变结构是邪恶的 一般来说。我也很确定,因为 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#编译器有一个错误。如果您处于这种情况下,我们会选择不一致的策略。今天的行为是:
如果通过方法突变的值变量是正常局部性,则正常突变
但是,如果它是一个悬挂的本地(因为它是匿名函数或迭代器块中的封闭变量),则是本地 是 实际上是作为仅读字段生成的,并确保在副本上发生突变的装备接管。
不幸的是,该规格对此事几乎没有指导。显然,某些事情被打破了,因为我们不一致,但是 对 要做的事情根本不清楚。
其他提示
当在编译时间中知道结构类型时,结构方法是内衬的,并且通过接口调用方法很慢,因此答案是:由于性能原因。