访问已修改闭包...但为什么?

5

在这里看到几个类似的问题,但似乎都不是我的问题...

我理解(或者说我认为我理解了)闭包的概念,并且知道什么会导致Resharper抱怨对修改后的闭包的访问,但在下面的代码中,我不明白我是如何违反闭包的。

因为primaryApps是在for循环的上下文中声明的,所以当我处理primaryApps时,primary不会改变。如果我在for循环外部声明了primaryApps,那么绝对会有闭包问题。但是为什么在下面的代码中会出现问题呢?

var primaries = (from row in openRequestsDataSet.AppPrimaries
                 select row.User).Distinct();

    foreach (string primary in primaries) {

        // Complains because 'primary' is accessing a modified closure
        var primaryApps = openRequestsDataSet.AppPrimaries.Select(x => x.User == primary);

Resharper是否只是聪明不够,无法发现这不是一个问题,还是在这里闭包是一个问题的原因我没有看到的?

1
您确定要使用“Select”而不是“Where”吗? - diceguyd30
Select 返回一个类型化数据行数组...where 返回它们的 IEnumerable。对于我正在做的事情,两者都可以使用;我读过一些文章说,在我使用的条件下,.Select 更快。也许是,也许不是,很难说。 - James King
我撤回之前的说法...因为我使用了lambda,所以我没有使用内置的datatable.select(),因此我没有得到一个类型化行数组。在这种情况下,你是正确的,我没有转换值,所以where更合适。 - James King
警告是关于一个陷阱,已经导致很多人遇到了很多问题 - 只需看看关于闭包和foreach循环的SO问题数量即可。因此,我认为这是一个好的警告。 - Ian Ringrose
我认为这是一个非常好的警告,我不会忽视它。只需要理解为什么在这种情况下会收到它,现在我已经明白了 :) - James King
4个回答

10
问题出在以下语句中:

因为 primaryApps 是在 for 循环的上下文中被声明的,所以在我处理 primaryApps 的同时,primary 不会改变。

Resharper 简单地无法100%验证这一点。引用闭包的 lambda 表达式被传递到此循环上下文之外的函数中:AppPrimaries.Select方法。该函数可能会储存结果委托表达式,稍后执行它并直接遇到迭代变量捕获问题。

正确地检测这是否可能发生是相当困难的,而且事实上不值得花费这种努力。相反,ReSharper 采取了安全路线,并警告潜在危险的迭代变量捕获。


此外,Visual Basic的编译器在完全相同的情况下会发出警告。C#团队考虑采用VB/Resharper的警告,但将决定推迟到下一个版本。 - Jonathan Allen
1
@Jonathan 不好意思打广告,这里有一篇深入解释VB.Net消息的文章http://blogs.msdn.com/b/jaredpar/archive/2007/07/26/closures-in-vb-part-5-looping.aspx - JaredPar
1
从你和Eric所说的来看,问题在于AppPrimaries.Select()是静态的,它可以将lambda表达式存储在静态变量中,而对AppPrimaries.CalculateSelectLambda()的另一次调用可能会导致执行被延迟,直到原始循环已经完成。此时,primary将保持最后一次通过循环设置的任何值。 - James King
@James 正确。尽管如果 Select 是一个实例方法而不是扩展方法,这同样适用。 - JaredPar

8
由于primaryApps在for循环的上下文中声明,因此在处理primaryApps时primary不会改变。 如果我在for循环之外声明了primaryApps,那么绝对会存在闭包问题。但是为什么在下面的代码中呢?
Jared是正确的;为了证明你的结论与你的前提不符,请让我们编写一个程序,在for循环的上下文中声明primaryApps,并且仍然存在捕获的循环变量问题。 很容易做到这一点。
static class Extensions
{
    public IEnumerable<int> Select(this IEnumerable<int> items, Func<int, bool> selector)
    {
        C.list.Add(selector);
        return System.Enumerable.Select(items, selector);
    }
}

class C
{
    public static List<Func<int, bool>> list = new List<Func<int, bool>>();
    public static void M()
    { 
        int[] primaries = { 10, 20, 30}; 
        int[] secondaries = { 11, 21, 30}; 

        foreach (int primary in primaries) 
        {
            var primaryApps = secondaries.Select(x => x == primary);
            // do something with primaryApps
        }
        C.N();
    }
    public static void N()
    {
        Console.WriteLine(C.list[0](10)); // true or false?
    }
}

"primaryApps" 的声明位置是完全无关紧要的。唯一重要的是,闭包可能会在循环中存活下来,因此有人可能在之后调用它,错误地期望闭包中捕获的变量是按值捕获的。

Resharper 无法知道 Select 的特定实现是否会将选择器存储以备后用;事实上,所有的实现都是这样做的。Resharper 怎么能知道它们恰好将其存储在不可访问的位置呢?


我想你是指“Resharper”,而不是“Reflector” ;) - Thomas Levesque
我想强调的是警告与primary在循环外声明有关,而不是primaryApps在循环内声明。 - Gabe

1
据我所知,Resharper每次访问foreach变量时都会生成警告,即使它实际上并没有引起闭包。

只有在 lambda 或其他延迟执行的上下文中使用变量时才会生效。 - James King

0

是的,我知道这只是一个警告……只是想知道我是否可以安全地忽略它:) 答案显然是“不可以”。 - James King

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