1. 理解C#中的闭包陷阱:foreach循环的坑
在C#开发中,闭包是一个强大但容易被误用的特性。特别是在foreach循环中使用闭包时,存在一个经典的陷阱,这个陷阱曾经让无数开发者踩坑。让我们从一个实际案例开始:
csharp复制var actions = new List<Action>();
foreach (var i in Enumerable.Range(1, 3))
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action();
}
你期望的输出是什么?1,2,3?实际上会输出3,3,3。这就是著名的foreach闭包陷阱。
1.1 闭包捕获机制解析
闭包在C#中通过捕获变量而不是值来工作。在foreach循环中,编译器会创建一个共享的循环变量(在上例中是i),所有闭包都捕获同一个变量引用。当循环结束后,这个变量的值是最后一个迭代值(3),所以所有闭包调用时都输出3。
这与for循环的行为不同:
csharp复制var actions = new List<Action>();
for (int i = 1; i <= 3; i++)
{
var temp = i; // 创建局部变量副本
actions.Add(() => Console.WriteLine(temp));
}
foreach (var action in actions)
{
action();
}
这个版本会正确输出1,2,3,因为每个迭代都创建了一个新的temp变量。
1.2 编译器如何处理foreach循环
理解这个陷阱的关键在于知道C#编译器如何处理foreach循环。编译器会将foreach转换为类似这样的结构:
csharp复制IEnumerator<int> enumerator = Enumerable.Range(1, 3).GetEnumerator();
try
{
int i; // 注意:这是在循环外部声明的
while (enumerator.MoveNext())
{
i = enumerator.Current;
actions.Add(() => Console.WriteLine(i));
}
}
finally
{
enumerator?.Dispose();
}
可以看到,循环变量i是在循环外部声明的,所有闭包都捕获同一个i变量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解决方案与最佳实践
2.1 临时变量解决方案
最简单的解决方案是在循环内部创建一个局部变量:
csharp复制foreach (var i in Enumerable.Range(1, 3))
{
var current = i; // 创建局部副本
actions.Add(() => Console.WriteLine(current));
}
这样每个闭包都捕获自己独立的current变量。
2.2 C# 5.0及更高版本的改变
值得注意的是,在C# 5.0中,微软改变了foreach循环的语义,使得循环变量在每次迭代中都是新的。这意味着在C# 5.0及更高版本中,原始的foreach闭包问题已经修复:
csharp复制// 在C# 5.0+中会输出1,2,3
var actions = new List<Action>();
foreach (var i in Enumerable.Range(1, 3))
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action();
}
2.3 多线程环境下的注意事项
在多线程环境中使用闭包需要格外小心:
csharp复制var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(() => Console.WriteLine(i)));
}
await Task.WhenAll
