foreach 标识符和闭包

Posted

技术标签:

【中文标题】foreach 标识符和闭包【英文标题】:The foreach identifier and closures 【发布时间】:2010-10-05 11:00:54 【问题描述】:

在以下两个sn-ps中,第一个安全还是必须做第二个?

安全是指每个线程都保证从创建线程的同一循环迭代中调用 Foo 上的方法?

或者您必须将对新变量“本地”的引用复制到循环的每次迭代中?

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
      
    Thread thread = new Thread(() => f.DoSomething());
    threads.Add(thread);
    thread.Start();

-

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
      
    Foo f2 = f;
    Thread thread = new Thread(() => f2.DoSomething());
    threads.Add(thread);
    thread.Start();

更新:正如 Jon Skeet 的回答所指出的,这与线程没有任何特别的关系。

【问题讨论】:

实际上我觉得它与线程有关,就好像您没有使用线程一样,您会调用正确的委托。在 Jon Skeet 的没有线程的示例中,问题是有 2 个循环。这里只有一个,所以应该没有问题...除非您不确切知道何时执行代码(这意味着如果您使用线程 - Marc Gravell 的回答完美地表明了这一点)。 Access to Modified Closure (2) 的可能重复项 @user276648 它不需要线程。将委托的执行推迟到循环之后就足以获得这种行为。 【参考方案1】:

编辑:这一切都在 C# 5 中发生了变化,改变了变量的定义位置(在编译器的眼中)。从 C# 5 开始,它们是相同的


C#5 之前

第二个是安全的;第一个不是。

使用foreach,变量被声明为在循环之外 - 即

Foo f;
while(iterator.MoveNext())

     f = iterator.Current;
    // do something with f

这意味着就闭包范围而言只有 1 个f,线程很可能会感到困惑——在某些实例上多次调用该方法,而在其他实例上则根本不调用。您可以在循环内部使用第二个变量声明来解决此问题:

foreach(Foo f in ...) 
    Foo tmp = f;
    // do something with tmp

这在每个闭包范围内都有一个单独的tmp,因此不存在此问题的风险。

这是问题的简单证明:

    static void Main()
    
        int[] data =  0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ;
        foreach (int i in data)
        
            new Thread(() => Console.WriteLine(i)).Start();
        
        Console.ReadLine();
    

输出(随机):

1
3
4
4
5
7
7
8
9
9

添加一个临时变量,它就可以工作了:

        foreach (int i in data)
        
            int j = i;
            new Thread(() => Console.WriteLine(j)).Start();
        

(每个数字一次,但当然不保证顺序)

【讨论】:

天哪……那个旧帖子让我很头疼。我一直希望 foreach 变量的范围在循环内。那是一次重要的 WTF 体验。 实际上这被认为是 foreach-loop 中的错误并在编译器中修复。 (与变量在整个循环中具有单个实例的 for 循环不同。) @Orlangur 我多年来一直与 Eric、Mads 和 Anders 进行直接对话。编译器遵循规范,所以是正确的。规范做出了选择。简单地说:那个选择被改变了。 此答案适用于 C# 4,但不适用于更高版本:“在 C# 5 中,foreach 的循环变量将在逻辑上位于循环内,因此闭包将关闭每次的变量。” (Eric Lippert) @Douglas 是的,我一直在纠正这些问题,但这是一个常见的绊脚石,所以:还有很多事情要做!【参考方案2】:

Pop Catalin 和 Marc Gravell 的答案是正确的。我只想添加一个指向my article about closures 的链接(其中涉及Java 和C#)。只是觉得它可能会增加一点价值。

编辑:我认为值得举一个不具有线程不可预测性的示例。这是一个简短但完整的程序,展示了这两种方法。 “不良行为”列表打印 10 次十次; “好行为”列表从 0 到 9 计数。

using System;
using System.Collections.Generic;

class Test

    static void Main() 
    
        List<Action> badActions = new List<Action>();
        List<Action> goodActions = new List<Action>();
        for (int i=0; i < 10; i++)
        
            int copy = i;
            badActions.Add(() => Console.WriteLine(i));
            goodActions.Add(() => Console.WriteLine(copy));
        
        Console.WriteLine("Bad actions:");
        foreach (Action action in badActions)
        
            action();
        
        Console.WriteLine("Good actions:");
        foreach (Action action in goodActions)
        
            action();
        
    

【讨论】:

谢谢 - 我附加了这个问题,说这与线程无关。 这也是您在您网站上的视频中进行的谈话之一csharpindepth.com/Talks.aspx 是的,我似乎记得我在那里使用了线程版本,其中一个反馈建议是避免线程 - 使用上面的示例会更清楚。 很高兴知道视频正在被观看 :) 即使知道变量存在于for 循环之外,这种行为也让我感到困惑。例如,在您的闭包行为示例中,***.com/a/428624/20774,变量存在于闭包之外但正确绑定。为什么会有所不同?【参考方案3】:

您需要使用选项 2,围绕变化的变量创建闭包将在使用变量时使用变量的值,而不是在创建闭包时使用。

The implementation of anonymous methods in C# and its consequences (part 1)

The implementation of anonymous methods in C# and its consequences (part 2)

The implementation of anonymous methods in C# and its consequences (part 3)

编辑:为了清楚起见,在 C# 中,闭包是“词法闭包”,这意味着它们不捕获变量的值,而是捕获变量本身。这意味着当为变化的变量创建闭包时,闭包实际上是对变量的引用,而不是其值的副本。

Edit2:如果有人有兴趣阅读有关编译器内部的内容,则添加所有博客文章的链接。

【讨论】:

我认为这适用于值和引用类型。【参考方案4】:

这是一个有趣的问题,我们似乎看到人们以各种方式回答。我的印象是第二种方式将是唯一安全的方式。我做了一个真正的快速证明:

class Foo

    private int _id;
    public Foo(int id)
    
        _id = id;
    
    public void DoSomething()
    
        Console.WriteLine(string.Format("Thread: 0 Id: 1", Thread.CurrentThread.ManagedThreadId, this._id));
    

class Program

    static void Main(string[] args)
    
        var ListOfFoo = new List<Foo>();
        ListOfFoo.Add(new Foo(1));
        ListOfFoo.Add(new Foo(2));
        ListOfFoo.Add(new Foo(3));
        ListOfFoo.Add(new Foo(4));


        var threads = new List<Thread>();
        foreach (Foo f in ListOfFoo)
        
            Thread thread = new Thread(() => f.DoSomething());
            threads.Add(thread);
            thread.Start();
        
    

如果你运行它,你会发现选项 1 绝对不安全。

【讨论】:

【参考方案5】:

在您的情况下,您可以通过将ListOfFoo 映射到一系列线程来避免此问题,而无需使用复制技巧:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)

    t.Start();

【讨论】:

【参考方案6】:

从 C# 版本 5(.NET 框架 4.5)开始,两者都是安全的。详情见这个问题:Has foreach's use of variables been changed in C# 5?

【讨论】:

【参考方案7】:
Foo f2 = f;

指向与

相同的引用
f 

所以没有失去也没有收获......

【讨论】:

这不是魔法。它只是捕捉环境。这里和 for 循环的问题是捕获变量发生了变异(重新分配)。 leppie:编译器会为你生成代码,一般不容易看到什么代码这到底是什么。这是编译器魔法的定义,如果有的话。 @leppie:我和康拉德在一起。编译器的长度感觉就像魔术一样,虽然语义已经明确定义,但它们并没有被很好地理解。什么不明白的东西可以与魔法相提并论? @Jon Skeet 你的意思是“任何足够先进的技术都与魔法无异”en.wikipedia.org/wiki/Clarke%27s_three_laws :) 它不指向引用。这是一个参考。它指向同一个对象,但它是不同的引用。

以上是关于foreach 标识符和闭包的主要内容,如果未能解决你的问题,请参考以下文章

你不知道的JavaScript(作用域和闭包)

JS 闭包

python的namespace和闭包(closure)

浅谈js闭包

前端进击的巨人:从作用域走进闭包

闭包函数