1. 闭包陷阱:从现象到本质
第一次遇到C#中foreach闭包陷阱时,我正为一个后台服务编写多线程日志处理器。代码逻辑很简单:遍历日志列表,为每条日志创建异步处理任务。使用for循环时,所有任务处理的都是最后一条日志——这个反直觉的结果让我调试了整整两小时。直到将for改为foreach,问题才神奇消失。这个经历促使我深入研究了闭包机制。
闭包的本质是函数与其词法环境的绑定。在C#中,当lambda表达式或匿名方法捕获外部变量时,编译器会生成一个隐藏的类(称为闭包类),将被捕获的变量提升为该类的字段。关键在于:闭包捕获的是变量本身而非变量值。这意味着:
- 若捕获的变量后续被修改,闭包内看到的是修改后的值
- 多个闭包捕获同一变量时,它们共享该变量的存储位置
csharp复制// 典型闭包陷阱示例
var actions = new List<Action>();
for (int i = 0; i < 3; i++) {
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) {
action(); // 输出3个3,而非预期的0,1,2
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. foreach的魔法:编译器如何解决问题
foreach的行为差异源于C#编译器的特殊处理。通过ILSpy反编译可以看到,foreach循环会被编译为以下模式:
csharp复制// 编译器生成的foreach结构
IEnumerator<int> enumerator = collection.GetEnumerator();
try {
while (enumerator.MoveNext()) {
int current = enumerator.Current; // 关键点:每次迭代声明新变量
actions.Add(() => Console.WriteLine(current));
}
} finally {
enumerator.Dispose();
}
与for循环不同,foreach的迭代变量(current)在每次迭代时都是重新声明的局部变量。这种设计本质上相当于自动
