zl程序教程

您现在的位置是:首页 >  其他

当前栏目

为什么编译器不能将foreach自动转换为for

转换自动编译器 for 为什么 不能 foreach
2023-09-14 09:10:45 时间

哲人告诉我们:勿过早优化

在编写C#代码的时候,开发者会发现:使用C#的foreach循环的性能会比对应的for循环要稍微慢一些。

foreach循环结构

 

for循环结构

 

我想说的第一件事是:这个性能差异,实在是太微小了,以至于可以完全忽略掉。
可千万别有这个想法:我如果把代码中的所有foreach循环改写为对应的for循环,程序的性能应该可以大大提升。这是不会发生的,因为循环的开销很少会出现在非基准程序(non-benchmark)花费大部分时间的地方。

今天我要说的主题不是如何通过放弃foreach循环来提高代码执行性能。我今天的主题是回答这个问题:”为什么编译器不会将foreach自动转换为相应的for,这样代码仍然是可读的,而且还可以利提升性能。”

原因是两个循环的本质并不相同。

枚举的语义是:不允许在进行枚举时更改要枚举的对象。如果你这样做,则枚举器将在下次相关调用时抛出InvalidOperationException异常。另一方面,在for循环中,你可以随意更改集合,这个是允许的。如果将项目插入到for循环内的集合中,则循环将继续进行,并且取决于插入发生的位置,你可能会对项目进行两次枚举。

如果编译器将foreach更改为for,则以前会引发异常的程序现在可以正常运行。你是否认为这是一项”改进”呢?(根据实际应用场景,可能使程序崩溃比产生不正确的结果更好。)

现在,编译器也许能够分析出你没有在循环内更改集合,但这项分析通常很难。例如,下面的循环代码会更改集合吗?

 

看起来上面的代码并没有改变集合。但是谁知道呢,万一这个target类似于如下的对象呢:

 

啊哦,你可能根本不知道o.GetHashCode()还会修改内部的ArrayList。因为它看起来是如此”无害”的操作啊!

如果SneakyContainer类是来自另一个程序集,则编译器必须假设最坏的情况,因为编译器根本不知道外部程序集的内部实现方法。

如果你觉得这还不够混乱的话,那么还有另一个例子。
ArrayList类未声明为sealed。因此,有人可以重写其IEnumerable.GetEnumerator并返回非标准的枚举器。例如,这是一个始终返回空枚举器的类:

 

你可能觉得:谁会那么无聊会重写枚举器呢?
好吧,这是一件很奇怪的事情,但是更普遍的是,开发者可以重写枚举器,以便添加过滤器或更改枚举的顺序。

因此,你甚至无法相信ArrayList确实是ArrayList,因为它内部可能是一个空的枚举器(ApparentlyEmptyArrayList)。

现在,如果编译器想要执行此优化,则不仅要证明枚举的对象未在枚举内部进行修改,还必须证明该对象确实是ArrayList而不是可能具有重写了GetEnumerator方法。

鉴于交叉汇编类的后期绑定性质,编译器可以证明这些要求的情况的数量确实非常有限,以至于不太可能在不更改语义的情况下安全地执行代码优化。

总结

我对C#不是很熟悉,我的建议是:始终使用一种你最为熟悉的语法结构,并保持一致。
例如,总是使用if/else而不是switch,if语句总是添加大括号,比较表达式始终用括号括起来等。
这样在写代码的时候,就会形成肌肉记忆,你都还没思考,代码就写出来了。脑细胞活力MAX。

最后

Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《Why the compiler can’t autoconvert foreach to for》