Действительно ли этот код вызывает «доступ к измененному закрытию»? - PullRequest
6 голосов
/ 11 февраля 2010

Используя следующий код, Решарпер говорит мне, что voicesSoFar и voicesNeededMaximum вызывают «доступ к измененному замыканию». Я читал об этом, но меня озадачивает то, что Решарпер предлагает исправить это, извлекая переменные прямо перед запросом LINQ. Но это то, где они уже есть!

Решарпер перестает жаловаться, если я просто добавлю int voicesSoFar1 = voicesSoFar сразу после int voicesSoFar = 0. Есть ли какая-то странная логика, которую я не понимаю, которая делает предложение Решарпера правильным? Или есть способ безопасно "получить доступ к измененным замыканиям" в подобных случаях, не вызывая ошибок?

// 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 ]

6 голосов
/ 11 февраля 2010

У вас очень неприятная проблема, возникающая из-за изменения внешней переменной в лямбда-выражении. Проблема заключается в следующем: если вы попытаетесь выполнить итерацию 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
3 голосов
/ 11 февраля 2010

Ну, сообщение об ошибке корректно настолько, что значение voicesSoFar не сохраняется во время операции. В чистом «функциональном» смысле (а лямбды действительно предназначены для того, чтобы действовать функционально) это будет сбивать с толку.

Например, интересный тест будет:

что произойдет, если я повторю запрос дважды?

Например:

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

Полагаю, вы видите ... 10, null - сбивает с толку. Следующее будет повторяться:

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)) {
    ...
}

Если вам нужно, вы, конечно, можете написать метод расширения многократного использования, использующий лямбду.

0 голосов
/ 11 февраля 2010

Я подозреваю, что изменение значения 'voicesSoFar' в TakeWhile вызывает проблему.

...