实际上,这是出于性能原因。BCL 团队对这一点进行了大量研究,然后才决定采用您正确称为可疑且危险的做法:使用可变值类型。
你问为什么这不会导致拳击。这是因为如果可以避免的话,C# 编译器不会在 foreach 循环中生成将内容装箱到 IEnumerable 或 IEnumerator 的代码!
当我们看到
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;
}
你会完全正确地期望改变 h 的尝试会失败,而且确实如此。编译器检测到您正在尝试更改具有待处理处理的对象的值,并且这样做可能会导致需要处理的对象实际上没有被处理。
现在假设你有:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h.Mutate();
}
这里会发生什么?如果 h 是一个只读字段,您可能会合理地期望编译器会做它所做的事情:制作一个副本,并改变该副本,以确保该方法不会丢弃需要处理的值中的内容。
然而,这与我们对这里应该发生的事情的直觉相冲突:
using (Enumerator enumtor = whatever)
{
...
enumtor.MoveNext();
...
}
我们希望在 using 块中执行 MoveNext会将枚举数移动到下一个,无论它是结构还是 ref 类型。
不幸的是,今天的 C# 编译器有一个错误。如果您处于这种情况,我们会选择不一致地遵循哪种策略。今天的行为是:
不幸的是,该规范在这个问题上几乎没有提供指导。很明显,有些东西是坏的,因为我们做的不一致,但什么是正确的做法并不明确。