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

问题描述

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

错误示例

csharp 复制代码
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 的无序数字输出。

实际输出

erlang 复制代码
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

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

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

ini 复制代码
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 表达式时,无需手动创建副本:

scss 复制代码
// 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 引入了本地函数,可以更清晰地处理变量捕获:

scss 复制代码
for (int i = 0; i < 100; i++) 
{
    ProcessItem(i);
​
    // 本地函数,自动捕获参数的副本
    void ProcessItem(int current)
    {
        Task.Run(() => DoTask(current));
    }
}

结论

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

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

相关推荐
源代码•宸4 分钟前
Leetcode—620. 有趣的电影&&Q3. 有趣的电影【简单】
数据库·后端·mysql·算法·leetcode·职场和发展
廋到被风吹走21 分钟前
【Spring】DispatcherServlet解析
java·后端·spring
码luffyliu1 小时前
系统优化:从压测到性能飞升
后端·压力测试
それども2 小时前
Spring Bean 的name可以相同吗
java·后端·spring
上进小菜猪2 小时前
基于深度学习的农业虫害自动识别系统:YOLOv8 的完整工程
后端
FAQEW3 小时前
若依(RuoYi-Vue)单体架构实战手册:自定义业务模块全流程开发指南
前端·后端·架构·若依二开
a努力。4 小时前
美团Java面试被问:Redis集群模式的工作原理
java·redis·后端·面试
计算机程序设计小李同学4 小时前
动漫之家系统设计与实现
java·spring boot·后端·web安全
布列瑟农的星空5 小时前
SSE与流式传输(Streamable HTTP)
前端·后端
开心就好20255 小时前
使用 HBuilder 上架 iOS 应用时常见的问题与应对方式
后端