“访问修改的闭包”是否可以通过理解语法来解决?

10

在第一个代码片段中,ReSharper 6.0 给了我 "访问修改的闭包" 警告,针对的是 dr 标识符。

private IEnumerable<string> GetTheDataTableStrings(DataTable dt) {
    foreach (DataRow dr in dt.Rows) {
        yield return GetStringFuncOutput(() => dr.ToString());
    }
}

我认为我已经基本理解了这个警告是想保护我免受的风险:在GetTheDataTableStrings输出被询问之前,dr会多次更改,因此调用者可能得不到我期望的输出/行为。

但是R#对于第二段代码片段没有给我任何警告。

private IEnumerable<string> GetTheDataTableStrings(DataTable dt) {
    return from DataRow dr in dt.Rows select GetStringFuncOutput(dr.ToString);
}

使用推导式语法时,如果我忽略这个警告/注意事项是否安全?

其他代码:

string GetStringFuncOutput(Func<string> stringFunc) {
    return stringFunc();
}

我之前已经对这段代码进行了简化和清理,以便展示。如果代码本身有什么问题阻碍了你讨论问题,请告诉我。 - lance
2个回答

22
首先,你对第一个版本的担忧是正确的。每个由该lambda创建的委托都是封闭在相同的变量上的,因此随着该变量的更改,查询的含义也会发生变化。
其次,我们很可能会在下一个版本的C#中修复这个问题;这是开发人员的主要痛点。
(更新:本答案编写于2011年。事实上,我们确实在C# 5中采取了下面描述的修复方法。)
在下一个版本中,每次运行“foreach”循环时,我们将生成一个新的循环变量,而不是每次都关闭相同的变量。这是一个“破坏性”的变化,但在绝大多数情况下,“破坏”将是修复而不是引起错误。
“for”循环不会被改变。
有关详细信息,请参见http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/
第三,查询理解版本没有问题,因为没有被修改的封闭变量。查询理解形式与您所说的相同:
return dt.Rows.Select(dr=>GetStringFuncOutput(dr.ToString));

lambda表达式没有闭包外部变量,因此没有变量会被意外修改。


很棒的答案和关于C#修复的好消息。你期望在哪个版本中得到它——C# 5还是6? - the_joric
1
@the_joric:目前还没有宣布C# 6的产品。我们计划在C# 5中进行修复。(我们必须重新调整闭包重写代码,以使异步/等待工作正常,因此想着干脆在同一时间解决这个问题。) - Eric Lippert
@phoog,是的,绝对是这样。它将改变编译器实现foreach的方式。看起来它已经更新了。http://msdn.microsoft.com/en-us/library/aa664754(v=vs.71).aspx 另请参阅:http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx - SolutionYogi
@SolutionYogi 在搜索C#4规范中各种形式的“close”和“closure”单词时,我无法找到任何描述循环变量关闭过程的内容。 我不明白VS 2003规范中对foreach循环(您提供的第一个链接)的描述如何相关。 - phoog
@SolutionYogi:很不幸,那份文档是错误的。你指向了2003年版本的文档。尽管当C# 5发布时它会神奇地变得准确,但对于2003年版本来说它并不准确;循环变量应该在外部声明。 - Eric Lippert
显示剩余4条评论

5
Resharper警告的问题已经在C#5.0和VB.Net 11.0中得到解决。以下是语言规范的摘录。请注意,规范默认情况下可以在安装了Visual Studio 2012的计算机上找到以下路径。
  • C:\Program Files(x86)\Microsoft Visual Studio 11.0\VB\Specifications\1033\Visual Basic Language Specification.docx
  • C:\Program Files(x86)\Microsoft Visual Studio 11.0\VC#\Specifications\1033\CSharp Language Specification.docx

C#语言规范版本5.0

8.8.4 foreach语句

v在while循环内部的位置对于任何出现在嵌入语句中的匿名函数如何捕获v很重要。

例如:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

如果v在while循环外声明,它将在所有迭代之间共享,并且在for循环后的值将是最终值13,这也是f调用时打印的结果。相反,由于每个迭代都有自己的变量v,因此第一次迭代中f捕获的变量将继续保持值7,这将被打印出来。(注意:早期版本的C#在while循环外部声明了v。)
Microsoft Visual Basic语言规范版本11.0
10.9.3 For Each...Next语句(注释)
语言版本10.0和11.0之间的行为略有不同。在11.0之前,每次循环没有为迭代创建新的迭代变量。只有当迭代变量被lambda或LINQ表达式捕获并在循环后调用时才能观察到此差异。
Dim lambdas As New List(Of Action)
For Each x In {1,2,3}
   lambdas.Add(Sub() Console.WriteLine(x)
Next
lambdas(0).Invoke()
lambdas(1).Invoke()
lambdas(2).Invoke()

到 Visual Basic 10.0,编译时会产生警告并打印出 "3" 三次。这是因为循环的所有迭代共享了唯一的变量 "x",并且所有三个 lambda 捕获了相同的 "x"。当 lambda 执行时,它们捕获到的变量 "x" 的值变成了 3。 从 Visual Basic 11.0 开始,它会打印出 "1, 2, 3"。这是因为每个 lambda 都捕获了不同的变量 "x"。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接