聊一聊 C# 中的闭包陷阱:foreach 循环的坑你还记得吗?

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

一段"人畜无害"的代码

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

csharp 复制代码
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,得手动"救"一下:

csharp 复制代码
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 循环依然有坑。

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

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

相关推荐
用户37215742613513 小时前
C# 实现 Excel 单元格保护:锁定公式并允许编辑指定区域
c#·ai编程·编程语言
公子小六18 小时前
基于.NET的Windows窗体编程之WinForms数据持久化(1)
windows·microsoft·c#·.net·winforms
三雷科技19 小时前
第6章第4节:自定义分词器
开发语言·c#
zhanghaha131421 小时前
C# 系列教程_12 字符串 string 保姆级系统教程(面向新手,大量示例 + 原理讲解)
开发语言·数据库·c#
Coder_Shenshen1 天前
罗克韦尔ACD工程归档离线变量树解析
算法·c#
寂寞的船2 天前
【Unity知识速递】第10期 Unity GC 零分配实战:从 UnityEngine.Pool 到手动 GC 控制的全链路优化
jvm·unity·c#·游戏引擎·lucene
1-1=02 天前
C# 数据结构和特殊类型
数据结构·c#·.net·.netcore
波力海苔夹心脆6754 天前
C# WPF 资源字典详解:ResourceDictionary、MergedDictionaries 与自定义控件案例
开发语言·c#·wpf·入门教程·resource·资源字典
AI导出鸭PC端4 天前
Perplexity批量导出没有PDF模式?AI导出鸭插件补上最后一块拼图
人工智能·pdf·c#·ai导出鸭
Risehuxyc4 天前
C# 选用更流行的语言版本
c#