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 标识符和闭包的主要内容,如果未能解决你的问题,请参考以下文章