如何在lambda表达式中捕获外部变量的值?

Erw*_*yer 16 c# lambda closures asynchronous task-parallel-library

我刚刚遇到以下行为:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i.ToString());
    });
}
Run Code Online (Sandbox Code Playgroud)

将导致一系列"错误:x",其中大多数x等于50.

同理:

var a = "Before";
var task = new Task(() => Debug.Print("Using value: " + a));
a = "After";
task.Start();
Run Code Online (Sandbox Code Playgroud)

将导致"使用价值:之后".

这显然意味着lambda表达式中的串联不会立即发生.在声明表达式时,如何在lambda表达式中使用外部变量的副本?以下内容不会更好(我承认这不一定是不连贯的):

var a = "Before";
var task = new Task(() => {
    var a2 = a;
    Debug.Print("Using value: " + a2);
});
a = "After";
task.Start();
Run Code Online (Sandbox Code Playgroud)

Pan*_*vos 26

这与lambdas有关,而不是线程.lambda捕获对变量的引用,而不是变量的值.这意味着,当您尝试使用在你的代码,它的价值将是任何存储在持续.

为避免这种情况,您应该在lambda启动时将变量的值复制到局部变量.问题是,启动任务有开销,第一个副本可能只在循环结束后执行.以下代码也将失败

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        var i1=i;
        Debug.Print("Error: " + i1.ToString());
    });
}
Run Code Online (Sandbox Code Playgroud)

正如James Manning所说,你可以在循环中添加一个局部变量并在那里复制循环变量.这样您就可以创建50个不同的变量来保存循环变量的值,但至少可以得到预期的结果.问题是,你确实得到了很多额外的分配.

for (var i = 0; i < 50; ++i) {
    var i1=i;
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i1.ToString());
    });
}
Run Code Online (Sandbox Code Playgroud)

最好的解决方案是将loop参数作为状态参数传递:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(o => {
        var i1=(int)o;
        Debug.Print("Error: " + i1.ToString());
    }, i);
}
Run Code Online (Sandbox Code Playgroud)

使用状态参数可以减少分配.查看反编译代码:

  • 第二个片段将创建50个闭包和50个代表
  • 第三个片段将创建50个盒装整数但只有一个代表

  • 对于第一个循环,'正确'修复(AFAICT)是做var i1 = i; 在循环内但在Task.Factory.StartNew之前.通过该更改,每个闭包将引用其自己的单独变量,您将获得正确的效果.但是,state参数避免了对闭包的需要,因此肯定更有效,但如果你只是想要正确的行为则不是必需的. (2认同)