C# 闭包捕获变量的经典问题分析

问题描述

在编写异步代码时,我们经常会遇到这样的情况:使用 for 循环创建多个异步任务,期望每个任务处理循环中的不同值,但最终输出结果却与预期不符。

错误示例

internal class CommonTestCode 
{
    public static void Print() 
    {
        for (int i = 0; i < 100; i++) 
        {
            Task.Run(() => 
            {
                DoTask(i);
            });
        }
    }

    private static void DoTask(int i)
    {
        Console.WriteLine(i);
    }
}

internal class Program 
{
    private static void Main(string[] args) 
    {
        CommonTestCode.Print();
        Console.ReadLine();
    }
}

预期输出

我们期望看到 0-99 的无序数字输出。

实际输出

3
3
100
6
8
6
6
8
13
13
13
100
100
100
...

大部分输出是 100,只有少量中间数字。

错误原因分析

1. 闭包捕获的是变量引用,而非当前值

在 C# 中,lambda 表达式(如 () => DoTask(i))会捕获变量的引用,而不是循环当时变量的具体值。

2. for 循环变量的作用域

for 循环中,迭代变量 i 是在循环外部声明的。这意味着所有异步任务共享同一个 i 的引用。

3. 异步任务执行时机滞后

Task.Run 会将任务放入线程池队列等待执行,而 for 循环本身是同步执行的,速度非常快。当循环执行完成时(i 从 0 递增到 100),大部分异步任务可能还未开始执行。

当这些任务最终执行 DoTask(i) 时,它们访问的 i 引用指向的是循环结束后的值 100,因此会打印大量 100

解决方案:捕获变量的副本

要解决这个问题,需要在循环内部创建一个局部变量副本,让每个异步任务捕获该副本的引用:

for (int i = 0; i < 100; i++) 
{
    int current = i; // 创建当前迭代的副本
    Task.Run(() => 
    {
        DoTask(current); // 捕获副本,而非原始 i
    });
}

优化后输出

优化后,每个异步任务都会捕获独立的 current 变量,其值为循环当时 i 的具体值,最终会打印出 0-99 的无序数字。

原理总结

概念 描述
闭包 捕获外部变量的匿名函数或 lambda 表达式
变量捕获 lambda 表达式捕获变量的引用,而非值
迭代变量作用域 for 循环变量在循环外部声明,所有迭代共享
异步执行 线程池任务执行时机晚于同步循环完成
解决方案 在循环内部创建局部变量副本,让每个任务捕获独立副本

C# 5.0+ 的 foreach 优化

值得注意的是,C# 5.0 对 foreach 循环进行了优化,将迭代变量的作用域调整到了循环内部。因此,在 foreach 循环中使用 lambda 表达式时,无需手动创建副本:

// C# 5.0+ 中,foreach 循环无需手动创建副本
foreach (var item in collection)
{
    Task.Run(() => DoTask(item)); // 自动捕获当前迭代的 item 值
}

for 循环仍保持原有行为,需要手动创建副本。

最佳实践

  1. for 循环中创建异步任务时,始终创建变量副本
  2. 了解闭包捕获的是引用,而非值
  3. 区分 forforeach 循环的变量作用域差异
  4. 使用 C# 7.0+ 的本地函数可以更清晰地处理变量捕获

本地函数优化方案

C# 7.0 引入了本地函数,可以更清晰地处理变量捕获:

for (int i = 0; i < 100; i++) 
{
    ProcessItem(i);

    // 本地函数,自动捕获参数的副本
    void ProcessItem(int current)
    {
        Task.Run(() => DoTask(current));
    }
}

结论

闭包变量捕获是 C# 中一个容易出错的特性,特别是在异步编程中。通过理解其工作原理,并采用正确的解决方案(创建变量副本),我们可以避免这类问题,写出更可靠的异步代码。

记住:for 循环中使用 lambda 表达式时,始终创建迭代变量的本地副本!

posted @ 2025-12-02 22:00  孤沉  阅读(15)  评论(0)    收藏  举报