1. 闭包与foreach循环的经典陷阱解析
第一次在线上环境遭遇这个bug时,我盯着日志里重复输出的最后一条数据,花了整整两小时才意识到是闭包在作祟。那天之后,C#中的foreach闭包问题就成了我代码审查时的重点关注对象。
闭包本质上是一个函数加上其访问的外部变量环境。在C#中,当lambda表达式或匿名方法捕获了外部变量时,编译器会生成一个隐藏的类来保存这些变量。而foreach循环中的迭代变量在C# 5.0之前是被视为"循环外"声明的,这就导致了一个反直觉的现象——所有延迟执行的闭包捕获的都是同一个变量引用。
csharp复制var actions = new List<Action>();
foreach (var i in Enumerable.Range(1, 3))
{
actions.Add(() => Console.WriteLine(i));
}
// 在C# 4.0及以下版本输出:3, 3, 3
foreach (var action in actions) action();
这个例子中,三个闭包捕获的是同一个i变量,当它们真正执行时,循环已经结束,i的值停留在3。我曾见过这种问题导致的事件处理器全部触发最后一项操作,或是并行任务处理同一份数据的灾难性结果。
关键点:闭包捕获的是变量本身而非变量值,这是理解这个陷阱的核心。就像一群人共用同一个记事本,最后看到的内容取决于最后写记事本的人。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C#版本差异与编译器行为揭秘
2.1 历史版本的行为差异
在C# 5.0之前,上述代码确实会产生意外结果。但微软在Roslyn编译器中修正了这个设计:
| C#版本 | foreach迭代变量作用域 | 闭包捕获行为 |
|---|---|---|
| ≤4.0 | 循环外部 | 共享同一变量 |
| ≥5.0 | 每次迭代新建 | 独立捕获 |
这个改变意味着在现代C#中,foreach循环的闭包行为变得符合大多数开发者的预期。但了解旧版本行为仍然重要,特别是维护遗留系统时。
2.2 编译器生成的IL代码分析
通过ILSpy反编译可以看
