C# 中的闭包一个小问题
using System; var funs = new Action[10]; for (var i = 0; i < 10; i++) funs[i] = () => Console.WriteLine(i); foreach (var fn in funs) fn();
猜测这段代码运行结果 1-9,实际运行结果为全部的 10 在 SharpLab 中查看
为什么呢,看反编译以后的代码是什么样子的
// 这里删除了一些不必要的代码引入和声明,为了减少篇幅 [CompilerGenerated] internal class Program { [CompilerGenerated] private sealed class <>c__DisplayClass0_0 { public int i; internal void <<Main>$>b__0() { Console.WriteLine(i); } } private static void <Main>$(string[] args) { Action[] array = new Action[10]; <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); <>c__DisplayClass0_.i = 0; while (<>c__DisplayClass0_.i < 10) { array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0); <>c__DisplayClass0_.i++; } Action[] array2 = array; int num = 0; while (num < array2.Length) { Action action = array2[num]; action(); num++; } } }
请看 <Main>$
函数中的代码,让我们看看他编译以后是如何对作用域引用的变量进行捕获的。
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); <>c__DisplayClass0_.i = 0; while (<>c__DisplayClass0_.i < 10) { array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0); <>c__DisplayClass0_.i++; }
实际主要就是 Main 函数中的这段代码,编译器创建了一个类 <>c__DisplayClass0_0
来捕获 Action 引用的变量,但是该类的实例是在循环外面,所以的 Action 都共享同样一个对象。所以解决这个问题十分简单,只需要让编译每次循环是都创建一个 <>c__DisplayClass0_0
类的实例对象就可以了。
有两种方法,我们先看第一种
// 这里只给出循环的代码 // loop for (var i = 0; i < 10; i++) { var num = i; # 在循环内声明变量,明确指明作用范围,指示编译器 funs[num] = () => Console.WriteLine(num); } // end
然后我们看看反编译后循环类的代码
while (num < 10) { <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); <>c__DisplayClass0_.num = num; array[num] = new Action(<>c__DisplayClass0_.<<Main>$>b__0); num++; }
ok,没问题
第二种方法
foreach (var i in Enumerable.Range(0, 10)) { var num = i; // 在循环内声明变量,明确指明作用范围,指示编译器 funs[i] = () => Console.WriteLine(num); } // end
这里是借助编译器对 foreach 的处理,所以反编译后的代码有些不同,下面是完整的 Main 函数中的内容
// Main Action[] array = new Action[10]; IEnumerator<int> enumerator = Enumerable.Range(0, 10).GetEnumerator(); try { while (enumerator.MoveNext()) { <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); // 重要的还是在循环中创建 <>c__DisplayClass0_.i = enumerator.Current; array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0); } } finally { if (enumerator != null) { enumerator.Dispose(); } } Action[] array2 = array; int num = 0; while (num < array2.Length) { Action action = array2[num]; action(); num++; } // end Main
因为基本我日常的循环都被 foreach 和 LINQ 取代了,所以我从来没有发现过这个问题,直到我遇上了。我认为这个很违反直觉,我知道在其他语言中也有类似的行为,但是在 C# 中我认为他确实是违法直觉的,我不知道这算不算一个 bug。
拿 JS 和 Py 举例,他们两个(JS 在 es6 以前使用没有 let 和 const 的时代)根本就没有块级作用域的概念,只有函数作用域,所以这样的表现能够理解。而 C# 不是的,他是明确有块级作用域的概念,所以不知道在这里又突然表现不同。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报