使用foreach遍历IEnumerable<int>时为何IL无box装箱指令?
结论
这段代码确实发生了装箱,你没看到box指令,是因为装箱根本不在你当前查看的Print方法IL里,而是藏在接口方法的实现内部。
具体原因
- 先搞清楚
List<int>的GetEnumerator有两个版本:- 公共实例方法:返回值是值类型
List<int>.Enumerator(结构体),直接在栈上分配枚举器,调用这个方法不会产生装箱,也是我们平时说List<T>遍历高性能的原因。 - 显式实现的接口方法:也就是
IEnumerable<int>.GetEnumerator(),返回值类型是IEnumerator<int>接口引用。这个方法的内部逻辑很简单:调用上面的公共方法拿到值类型枚举器,把它装箱后,返回装箱对象的接口引用。
- 公共实例方法:返回值是值类型
- 你的
Print方法参数是IEnumerable<int>接口类型,编译器在这里只会调用接口定义的GetEnumerator(),也就是上面说的第二个版本。这个方法的返回值本身就是接口引用,所以你在Print的IL里看到callvirt调用后直接把返回值存到IEnumerator<int>类型的局部变量,当然不需要额外加box指令——装箱步骤已经在被调用的那个方法内部执行完了。 - 你之前的认知“值类型转接口类型会产生装箱”是完全正确的,只是这个装箱发生的位置不在当前方法的IL里,所以你用ildasm看当前方法找不到对应的
box指令。
对比验证
你可以改下代码做对照:把Print方法的参数从IEnumerable<int>改成具体的List<int>类型,再看生成的IL:
- 这时候编译器会调用
List<int>的公共GetEnumerator方法,局部变量类型就是值类型的List<int>.Enumerator - 全程没有任何
box指令,也不会出现接口类型的枚举器变量,这才是零装箱的foreach遍历。
另外补充:你贴的IL里后续对MoveNext()、Current、Dispose()的调用,都是针对已经装箱完成的枚举器引用做的,不会再产生额外的装箱开销。
内容的提问来源于stack exchange,提问作者walruz
相关产品推荐
相关产品推荐

