大家好,我是刚子,一个写了六年代码的.NET程序员。今天咱们聊一个经典老坑——C# 里 foreach 配合闭包(Lambda、匿名方法)时的诡异行为。如果你是 .NET 大佬,肯定被它咬过;如果你刚入行,听完这个故事,以后面试、写代码都能少踩一个雷。

一段“人畜无害”的代码

先上代码,你猜猜输出什么?

var actions = new List<Action>();

foreach (var i in new[] { 1, 2, 3 })
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var action in actions)
{
    action();
}

直觉告诉我们:1, 2, 3 挨个输出。
但如果你用的是 C# 4.0 及以前(或者 .NET Framework 4.5 之前),实际输出是:3, 3, 3。

惊不惊喜?意不意外?

为什么会这样?

关键点:闭包捕获的是变量,不是变量的值

在旧版 C# 中,foreach 循环只声明一个循环变量 i,每次迭代只是修改这个 i 的值。当你用 Lambda () => Console.WriteLine(i) 捕获 i 时,捕获的是同一个变量引用。等到循环结束、真正执行 action 的时候,i 的值已经是最后一次迭代的值(3),所以三个委托打印出来全是 3。

你可以理解为:循环一直在跟变量 i“原地打转”,而所有的匿名函数都紧紧抓着这一个 i 不放。

当年的“经典解法”

在 C# 5.0 之前,要想得到预期的 1,2,3,得手动“救”一下:

foreach (var i in new[] { 1, 2, 3 })
{
    var temp = i;   // 局部副本
    actions.Add(() => Console.WriteLine(temp));
}

每次循环新建一个局部变量 temp,它只在当前迭代“存活”,每个委托捕获的是自己专属的 temp,互不干扰。输出就正常了。

这种写法在当时几乎是标准操作,写多了肌肉记忆都形成了。

转折:C# 5.0 终于“拨乱反正”

从 C# 5.0 开始(对应 .NET Framework 4.5 / Visual Studio 2012),语言团队改了 foreach 的行为——每次迭代都会产生一个新的循环变量实例

简单说:上面那段“人畜无害”的代码,现在在任何现代 C# 版本(5.0+)中运行,输出直接就是 1,2,3。你不再需要手动 temp 了。

注意:这个改变只针对 foreach,经典的 for 循环依然保持旧行为(因为 for 里的 i 还是那个单变量)。
比如 for (int i = 0; i < 3; i++) { actions.Add(() => Console.WriteLine(i)); } 输出仍然是 3,3,3,需要自己加 temp

为什么还有必要聊这个“旧坑”?

虽然现代 C# 里 foreach 已经没这个坑了,但我觉得每个 .NET 开发者还是得知道这件事,原因有三:

  1. 维护老项目:你可能碰到还在用 .NET 4.0 或更低版本的项目(比如某些工控、遗留系统),闭包陷阱照旧存在。到时候你一脸懵地查半天,不如现在就记住。
  2. 面试高频题:面试官最爱问“foreach 闭包的坑,说说你的理解”。你要是直接说“C# 5.0 以后没问题了”,然后讲不清历史原因,反而显得像背答案。把背后的“变量捕获机制”讲透,才是加分项。
  3. 理解闭包本质:这个坑本质上帮你理解了“闭包捕获的是变量引用,而非值”。理解了这点,你在写异步、事件订阅、Task 并发时,对变量的作用域会有更清晰的认识。

我自己当年踩坑的回忆

我记得刚工作那会儿,用 .NET 4.0 写一个批量下载的程序。循环发起多个 WebClient 异步请求,用 Lambda 回调里打印当前索引。结果发现所有回调打印的都是最后一个索引,当时完全懵了,调试半天,最后翻 Stack Overflow 才知道 foreach 这个“特性”。

从那以后,我养成了一个习惯:只要在循环里写 Lambda,下意识看一眼捕获的变量是不是会变。后来升级到 C# 5.0 之后,这个习惯反而被惯坏了,偶尔写 for 循环时又栽跟头——因为 for 没变啊!

所以现在我给自己的原则是:

  • foreach 时放心捕获循环变量(5.0+)
  • for 时依然手动 temp,或者直接转成 foreach 避免犯错
  • 如果代码需要兼容老版本,那就一律手动 temp,省心

总结一句

C# 中的闭包陷阱,本质是“变量捕获”规则与开发者直觉的冲突。foreach 的历史变化是个经典的案例:从“一个变量打天下”到“迭代一次生一个新变量”,这个改进拯救了无数人的头发。

如果你还在用老版本,记住那个 temp 小技巧。如果你已经升级到新版本,也别忘了 for 循环依然有坑。

好了,今天先聊到这。你当年在这个坑里扑腾过吗?评论区聊聊你的“惨痛”经历吧~

如果你觉得文章有用,点个赞、转给身边正在写代码的兄弟。