此代码是否真的导致“访问修改后的闭包”问题?

时间:2010-02-11 04:58:38

标签: c# .net linq closures

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

如果我只是在int voicesSoFar1 = voicesSoFar之后立即添加int voicesSoFar = 0,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));

3 个答案:

答案 0 :(得分:6)

将外部变量变为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

答案 1 :(得分:3)

嗯,错误消息是正确的,因为在操作期间不会保留voicesSoFar的值。在纯粹的“功能性”术语中(而lambda实际上是为了在功能上起作用),这将是令人困惑的。

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

  

如果我重复两次查询会怎么样?

例如:

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的可重用扩展方法。

答案 2 :(得分:0)

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