【问题标题】:Does this code really cause an "access to modified closure" problem?这段代码真的会导致“访问修改后的闭包”问题吗?
【发布时间】:2010-02-11 04:58:37
【问题描述】:

使用以下代码,Resharper 告诉我 voicesSoFarvoicesNeededMaximum 导致“访问修改后的闭包”。我读到了这些,但令我困惑的是,Resharper 建议通过在 LINQ 查询之前提取变量来解决这个问题。但他们已经在那儿了!

如果我只是在int voicesSoFar = 0 之后添加int voicesSoFar1 = voicesSoFar,Resharper 就会停止抱怨。是否有一些我不明白的奇怪逻辑使 Resharper 的建议正确?或者有没有办法在这种情况下安全地“访问修改后的闭包”而不会导致错误?

// this takes voters while we have less than 300 voices    
int voicesSoFar = 0;    
int voicesNeededMaximum = 300;    
var eligibleVoters =
    voters.TakeWhile((p => (voicesSoFar += p.Voices) < voicesNeededMaximum));

【问题讨论】:

  • 我关闭了这个 Resharper 警告。在修改捕获的变量时您当然必须小心,但这是对语言的良好理解胜过遵循死记硬背规则的地方之一。 C# 允许这样做是有原因的——它很有用。

标签: c# .net linq closures


【解决方案1】:

您遇到了一个非常讨厌的问题,即将外部变量更改为 lambda 表达式。问题是这样的:如果您尝试迭代 eligibleVoters 两次(foreach(var voter in eligibleVoters) { Console.WriteLine(voter.Name); } 和紧随其后的(foreach(var voter in eligibleVoters) { Console.WriteLine(voter.Name); }),您将不会看到相同的输出。从函数式编程的角度来看,这是不对的。

这是一个扩展方法,它将累积直到累加器上的某些条件为真:

public static IEnumerable<T> TakeWhileAccumulator<T, TAccumulate>(
    this IEnumerable<T> elements,
    TAccumulate seed,
    Func<TAccumulate, T, TAccumulate> accumulator,
    Func<TAccumulate, bool> predicate
) {
    TAccumulate accumulate = seed;
    foreach(T element in elements) {
        if(!predicate(accumulate)) {
            yield break;
        }
        accumulate = accumulator(accumulate, element);
        yield return element;
    }
}

用法:

var eligibleVoters = voters.TakeWhileAccumulator(
                         0,
                         (votes, p) => votes + p.Voices, 
                         i => i < 300
                     );

所以上面说的是在我们积累不到 300 票的情况下积累声音。

然后用:

foreach (var item in eligibleVoters) { Console.WriteLine(item.Name); }
Console.WriteLine();
foreach (var item in eligibleVoters) { Console.WriteLine(item.Name); }

输出是:

Alice
Bob
Catherine

Alice
Bob
Catherine

【讨论】:

    【解决方案2】:

    好吧,错误消息是正确的,因为voicesSoFar 的值在操作期间没有被保留。用纯粹的“函数式”术语(而 lambdas 真正设计为在函数式上起作用)会令人困惑。

    例如,一个有趣的测试是:

    如果我重复查询两次会发生什么?

    例如:

    int count = voters.Count();
    var first = voters.FirstOrDefault();
    

    我相信你可以看到...10null - 令人困惑。以下是可重复的:

    public static IEnumerable<Foo> TakeVoices(
        this IEnumerable<Foo> voices, int needed)
    {
        int count = 0;
        foreach (Foo voice in voices)
        {
            if (count >= needed) yield break;
            yield return voice;
            count += voice.Voices;
        }
    }
    ....
    foreach(var voice in sample.TakeVoices(numberNeeded)) {
        ...
    }
    

    如果您需要,当然可以编写一个采用 lambda 的可重用扩展方法。

    【讨论】:

      【解决方案3】:

      我怀疑修改 TakeWhile 中的“voicesSoFar”值会导致问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-02-05
        • 1970-01-01
        • 1970-01-01
        • 2011-09-29
        • 2011-07-28
        • 2010-09-23
        • 2015-11-08
        相关资源
        最近更新 更多