C# foreach 标识符和闭包

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/512166/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 06:17:17  来源:igfitidea点击:

The foreach identifier and closures

c#enumerationclosures

提问by xyz

In the two following snippets, is the first one safe or must you do the second one?

在以下两个片段中,第一个是安全的还是必须执行第二个?

By safe I mean is each thread guaranteed to call the method on the Foo from the same loop iteration in which the thread was created?

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

Or must you copy the reference to a new variable "local" to each iteration of the loop?

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

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();
}

Update:As pointed out in Jon Skeet's answer, this doesn't have anything specifically to do with threading.

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

采纳答案by Marc Gravell

Edit: this all changes in C# 5, with a change to where the variable is defined (in the eyes of the compiler). From C# 5 onwards, they are the same.

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



Before C#5

在 C#5 之前

The second is safe; the first isn't.

二是安全;第一个不是。

With foreach, the variable is declared outsidethe loop - i.e.

使用foreach,变量在循环声明- 即

Foo f;
while(iterator.MoveNext())
{
     f = iterator.Current;
    // do something with f
}

This means that there is only 1 fin terms of the closure scope, and the threads might very likely get confused - calling the method multiple times on some instances and not at all on others. You can fix this with a second variable declaration insidethe loop:

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

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

This then has a separate tmpin each closure scope, so there is no risk of this issue.

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

Here's a simple proof of the problem:

这是问题的简单证明:

    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();
    }

Outputs (at random):

输出(随机):

1
3
4
4
5
7
7
8
9
9

Add a temp variable and it works:

添加一个临时变量,它的工作原理:

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

(each number once, but of course the order isn't guaranteed)

(每个号码一次,但当然不能保证顺序)

回答by Matze

Foo f2 = f;

points to the same reference as

指向相同的引用

f 

So nothing lost and nothing gained ...

所以没有失去也没有得到什么......

回答by Pop Catalin

Your need to use option 2, creating a closure around a changing variable will use the value of the variable when the variable is used and not at closure creation time.

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

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

C#中匿名方法的实现及其后果(第1部分)

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

C#中匿名方法的实现及其后果(第2部分)

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

C#中匿名方法的实现及其后果(第3部分)

Edit: to make it clear, in C# closures are "lexical closures" meaning they don't capture a variable's value but the variable itself. That means that when creating a closure to a changing variable the closure is actually a reference to the variable not a copy of it's value.

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

Edit2: added links to all blog posts if anyone is interested in reading about compiler internals.

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

回答by Jon Skeet

Pop Catalin and Marc Gravell's answers are correct. All I want to add is a link to my article about closures(which talks about both Java and C#). Just thought it might add a bit of value.

Pop Catalin 和 Marc Gravell 的回答是正确的。我只想添加一个链接到我的关于闭包的文章(讨论 Java 和 C#)。只是认为它可能会增加一些价值。

EDIT: I think it's worth giving an example which doesn't have the unpredictability of threading. Here's a short but complete program showing both approaches. The "bad action" list prints out 10 ten times; the "good action" list counts from 0 to 9.

编辑:我认为值得举一个没有线程不可预测性的例子。这是一个简短但完整的程序,显示了这两种方法。“坏动作”列表打印出 10 次 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();
        }
    }
}

回答by JoshBerke

This is an interesting question and it seems like we have seen people answer in all various ways. I was under the impression that the second way would be the only safe way. I whipped a real quick proof:

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

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();
        }
    }
}

if you run this you will see option 1 is definetly not safe.

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

回答by Ben James

In your case, you can avoid the problem without using the copying trick by mapping your ListOfFooto a sequence of threads:

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

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

回答by lex82

Both are safe as of C# version 5 (.NET framework 4.5). See this question for details: Has foreach's use of variables been changed in C# 5?

从 C# 版本 5(.NET 框架 4.5)开始,两者都是安全的。有关详细信息,请参阅此问题:C# 5 中是否更改了 foreach 对变量的使用?