foreach (var s in strings)
{
query = query.Where(i => i.Prop == s); // access to modified closure
...
}
由于修改了闭包,以上代码将导致查询中的所有
Where
子句都基于s
的最终值。 br /> 如此处所述,这是因为在编译器中将上述
s
循环中声明的foreach
变量在编译器中进行了如下翻译:string s;
while (enumerator.MoveNext())
{
s = enumerator.Current;
...
}
而不是像this:
while (enumerator.MoveNext())
{
string s;
s = enumerator.Current;
...
}
如此处所指出的,在循环外声明变量没有性能优势,在正常情况下,我能想到的唯一原因是这是如果您打算在循环范围之外使用变量:
string s;
while (enumerator.MoveNext())
{
s = enumerator.Current;
...
}
var finalString = s;
但是在
foreach
循环中定义的变量不能在循环之外使用:foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.
因此,编译器以某种方式声明变量,使其极易出现通常难以查找和调试的错误,而没有产生可察觉的好处。 >
是否可以使用
foreach
循环执行某些操作,如果它们使用内部作用域变量进行编译则无法执行,或者这只是在匿名方法和lambda表达式可用之前做出的任意选择还是通用,并且自那时以来没有进行过修订?#1 楼
编译器声明该变量的方式使其很容易出错,而该错误通常很难查找和调试,并且不会产生明显的收益。
您的批评是完全有道理。
我在这里详细讨论这个问题:
关闭被认为有害的循环变量
可以用这种方式处理foreach循环,如果它们是使用内部作用域变量编译的,则无法实现?还是这只是在匿名方法和lambda表达式可用或通用之前做出的任意选择,并且此后从未进行过修改?
后者。实际上,C#1.0规范没有说明循环变量是在循环体内还是在循环体内,因为它没有明显的区别。在C#2.0中引入闭包语义时,选择将循环变量置于循环之外,与“ for”循环一致。
我可以说所有人都为该决定感到遗憾。这是C#中最糟糕的“陷阱”之一,我们将进行重大更改来修复它。在C#5中,foreach循环变量在逻辑上将位于循环体内,因此每次闭合都将获得新的副本。
for
循环将不会更改,并且更改不会更改“反向移植”到以前的C#版本。因此,在使用此惯用语时,您应该继续小心。评论
实际上,我们确实推迟了C#3和C#4的更改。当我们设计C#3时,我们确实意识到问题(在C#2中已经存在)会变得更糟,因为会有太多的lambda(和查询)理解,这是变相的lambdas),这要归功于LINQ。我感到遗憾的是,我们等待该问题变得很严重,以至于不能及时解决该问题,而不是在C#3中进行修复。
–埃里克·利珀特
2012年1月17日19:21
现在,我们将不得不记住,foreach是“安全的”,但是for不是。
–嬉皮士
2012年1月18日下午5:58
@michielvoo:从不向后兼容的意义上讲,这种变化正在打破。使用较旧的编译器时,新代码将无法正确运行。
–嬉皮士
2012年1月18日9:00
@Benjol:不,这就是为什么我们愿意接受它。乔恩·斯凯特(Jon Skeet)向我指出了一个重要的突破性变化方案,即有人用C#5编写代码,然后对其进行测试,然后与仍在使用C#4的人共享该代码,然后他们天真的认为它是正确的。希望受这种情况影响的人数很少。
–埃里克·利珀特
2012年1月19日14:20
顺便说一句,ReSharper始终抓住了这一点,并将其报告为“对修改后的闭包的访问”。然后,通过按Alt + Enter,它甚至会自动为您修复代码。 jetbrains.com/resharper
–迈克·张伯伦
2012年3月21日在22:57
#2 楼
埃里克·利珀特(Eric Lippert)在他的博客文章中完全涵盖了您所要问的问题:结束被认为有害的循环变量及其后继。对我来说,最有说服力的论据是,每次迭代中都有新变量与
for(;;)
样式循环不一致。您是否希望在int i
的每次迭代中都有一个新的for (int i = 0; i < 10; i++)
?这种行为最常见的问题是对迭代变量进行闭包,并且有一个简单的解决方法:
foreach (var s in strings)
{
var s_for_closure = s;
query = query.Where(i => i.Prop == s_for_closure); // access to modified closure
我的博客文章有关此问题:在C#中关闭foreach变量。
评论
最终,人们在写这篇文章时真正想要的不是拥有多个变量,而是覆盖价值。在一般情况下,很难想到一种有用的语法。
–Random832
2012年1月17日17:44
是的,无法按该值结束,但是有一个非常简单的解决方法,我刚刚编辑了要包含的答案。
– Krizz
2012年1月17日17:50
C#中对引用的关闭太糟糕了。如果默认情况下关闭了值,我们可以轻松地用ref指定关闭变量。
– Sean U
2012年1月17日17:53
@Krizz,在这种情况下,强制一致性比不一致要有害得多。它应该像人们期望的那样“正常工作”,并且很明显,人们在使用foreach而不是for循环时期望有所不同,因为在我们知道访问修改后的闭包问题(例如我自己)之前遇到问题的人数。
–安迪
2012年1月17日19:41
@ Random832不了解C#,但是在Common LISP中有一种语法,它指出任何具有可变变量和闭包的语言也都可以(不,必须)。我们要么封闭对变更位置的引用,要么封闭其在给定时间点所具有的值(封闭的创造)。本文讨论了Python和Scheme中的类似内容(为refs / vars剪切,而cute为将评估值保留在部分评估的闭包中)。
–尼斯
2012年1月18日在18:09
#3 楼
被这个问题咬伤后,我有一个习惯,就是将局部定义的变量包含在最里面的范围内,该范围用于传递给任何闭包。在您的示例中:foreach (var s in strings)
query = query.Where(i => i.Prop == s); // access to modified closure
我这样做:
foreach (var s in strings)
{
string search = s;
query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}
一旦有了这种习惯,就可以避免在极少数情况下,您实际上打算绑定到外部作用域。老实说,我认为我从未这样做过。
评论
这是典型的解决方法。感谢您的贡献。 Resharper足够聪明,可以识别这种模式并将其引起您的注意,这很好。我已经有一段时间没有被这种模式所困扰,但是既然用埃里克·利珀特(Eric Lippert)的话来说,就是“我们得到的最常见的错误报告”,我很想知道为什么而不是如何避免它。
–StriplingWarrior
2012年1月17日17:53
#4 楼
在C#5.0中,此问题已修复,可以关闭循环变量并获得所需的结果。语言规范中说:
8.8.4 foreach语句
(...)
然后扩展形式为
foreach (V v in x) embedded-statement
的foreach语句to:
{
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded-statement
}
}
finally {
… // Dispose e
}
}
(...)
v
在while循环中的位置对于它的状态很重要被
嵌入语句中发生的任何匿名函数捕获。例如:
int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();
如果在while循环之外声明了
v
,它将在所有迭代之间共享,并且它在for循环之后的值将是
最终值
13
,这是f
的调用将打印的内容。因为每个迭代都有自己的变量
v
,所以f
在第一次迭代中捕获的变量将继续保留值
7
,它将被打印。 (注意:较早版本的C#在while循环外声明了
v
。)评论
为什么此早期C#版本在while循环内声明v?msdn.microsoft.com/zh-CN/library/aa664754.aspx
– colinfang
13年4月29日在16:00
@colinfang请确保阅读Eric的回答:C#1.0规范(在您的链接中,我们谈论的是VS 2003,即C#1.2)实际上并未说明循环变量是在循环体内还是在循环体内,因为它没有明显的区别。 。在C#2.0中引入闭包语义时,选择将循环变量置于循环之外,与“ for”循环一致。
–保罗·莫雷蒂(Paolo Moretti)
13年4月29日在18:35
因此,您是说链接中的示例当时不是确定的规范?
– colinfang
13年4月29日在18:47
@colinfang它们是确定的规范。问题在于我们正在谈论的是稍后(使用C#2.0)引入的功能(即函数闭包)。当C#2.0出现时,他们决定将循环变量放在循环之外。然后,他们再次使用C#5.0改变了主意:)
–保罗·莫雷蒂(Paolo Moretti)
13年4月29日在22:01
评论
String s怎么了? foreach(以字符串形式){...}?OP的@BradChristie并不是真正在谈论foreach,而是关于lamda表达式导致与OP所示代码相似的代码。
@BradChristie:可以编译吗? (错误:对于我来说,在foreach语句中都必须同时输入类型和标识符)
@JakobBotschNielsen:这是lambda的封闭局部。您为什么要假设它完全要放在堆栈中?它的寿命比堆栈框架更长!
@EricLippert:我很困惑。我了解到lambda捕获了对foreach变量的引用(该变量在循环外部内部声明),因此最终将其与最终值进行比较;我得到。我不明白的是,在循环内部声明变量将如何有所不同。从编译器编写器的角度来看,无论声明是在循环内还是循环外,我都只在堆栈上分配一个字符串引用(var's')。我当然不想每次迭代都将新引用推入堆栈!