闭包捕获栈变量疑问:编译器堆对象处理及修复代码原理
闭包与循环变量捕获的常见问题解答
原错误代码
for (int i = 0; i < 10; i++) { Task.Factory.StartNew(() => Console.WriteLine(i)); } Console.ReadLine();
作者论述
这段代码意图打印0到9的数字,但运行后大概率输出10个10。原因在于闭包:编译器需捕获局部变量i并放入堆上的编译器生成对象,供lambda引用。由于i声明在循环外,仅创建一个对象存储i的值,所有任务共享该对象,循环结束时i为10,故所有任务输出10。
用户困惑
- i是存储在栈上的值类型,编译器如何在堆上创建包含该栈变量内存引用的对象?
- 作者提供的修复代码如下:
for (int i = 0; i < 10; i++) { int toCaptureI = i; Task.Factory.StartNew(() => Console.WriteLine(toCaptureI)); } Console.ReadLine();
若每次循环都在堆上创建对象持有toCaptureI的引用,而toCaptureI每次迭代在栈上赋新值,为何不会出现与错误代码相同的问题?
问题1解答
编译器处理闭包时,不会直接引用栈上的原始i变量。实际逻辑是:
- 自动生成一个编译器专属的类,类里包含一个和i同类型的字段。
- 把原本栈上的i变量替换成这个类实例的字段——从循环开始,i就不再是栈上的值,而是堆上对象的成员。
- lambda表达式会持有这个堆对象的引用,后续访问i时,实际是访问该对象的字段。
说白了,栈上的i已经被“转移”到堆上的捕获对象里了,lambda引用的是堆对象的字段,而非原本栈上的变量。
问题2解答
修复代码的核心是toCaptureI是循环内部声明的变量,编译器会为每次循环迭代生成独立的捕获对象:
- 每一次循环都会新建一个捕获对象,里面包含对应
toCaptureI的字段。 toCaptureI = i是把当前迭代的i值复制到这个新对象的字段中。- 每个lambda都持有属于自己的捕获对象引用,它们访问的是不同对象的字段,这些字段的值就是当前迭代时i的快照,不会被后续循环的i值覆盖。
简单来说,错误代码是所有lambda共享同一个捕获对象,修复代码是每个lambda有专属的捕获对象,各自保存了迭代时的i值,自然不会全输出10。
内容的提问来源于stack exchange,提问作者user22155685
相关产品推荐
相关产品推荐

