2010-02-11 12 views
6

Następujący kod, Resharper mówi, że voicesSoFar i voicesNeededMaximum powodują "dostęp do zmodyfikowanego zamknięcia". Czytałem o tym, ale to, co mnie zastanawia, polega na tym, że Resharper sugeruje naprawienie tego poprzez wyodrębnienie zmiennych tuż przed zapytaniem LINQ. Ale tam już są!Czy ten kod naprawdę powoduje problem "dostęp do zmodyfikowanego zamknięcia"?

Resharper przestaje narzekać, jeśli dodam tylko int voicesSoFar1 = voicesSoFar zaraz po int voicesSoFar = 0. Czy jest jakaś dziwna logika, której nie rozumiem, która sprawia, że ​​sugestia Resharpera jest poprawna? Czy istnieje sposób, aby bezpiecznie "uzyskać dostęp do zmodyfikowanych zamknięć" w takich przypadkach, nie powodując błędów?

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

Wyłączam ostrzeżenie o Resharper. Na pewno musisz zachować ostrożność podczas modyfikowania przechwyconych zmiennych, ale jest to jedno z tych miejsc, w których dobre zrozumienie języka wygrywa z regułą. Jest powód, dla którego C# to umożliwia - jest to użyteczne. –

Odpowiedz

6

Masz bardzo nieprzyjemny problem, który powstaje w wyniku mutacji zmiennej zewnętrznej do wyrażenia lambda. Problem jest taki: jeśli próbujesz iteracyjne eligibleVoters dwukrotnie (foreach(var voter in eligibleVoters) { Console.WriteLine(voter.Name); } i bezpośrednio po (foreach(var voter in eligibleVoters) { Console.WriteLine(voter.Name); }) nie będzie widać ten sam wyjściowy, który nie jest w porządku z funkcjonalnego punktu widzenia programowania

Oto metoda rozszerzenie, które będzie.. zgromadzić aż jakiś stan na akumulatorze jest prawdziwe:

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; 
    } 
} 

Zastosowanie:

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

Zatem powyższe mówi zgromadzić głosy podczas gdy udało nam się zebrać mniej niż 300 głosów.

następnie:

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

wyjściowa wynosi:

Alice 
Bob 
Catherine 

Alice 
Bob 
Catherine 
+0

Czy istnieje dobry sposób na obsłużenie tego? –

+0

Tak, napisz inną metodę rozszerzenia. Zobacz moją edycję. – jason

0

Podejrzewam, że modyfikując wartość „voicesSoFar” w TakeWhile jest przyczyną problemu.

3

Dobrze, komunikat o błędzie jest poprawne o tyle, że wartość voicesSoFar nie jest zachowana podczas operacji. W czysto "funkcjonalnych" terminach (i lambdy są naprawdę zaprojektowane do działania funkcjonalnie) będzie to mylące.

Na przykład ciekawy badanie byłoby:

co się stanie, jeśli dwa razy iteracyjne zapytania?

Na przykład:

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

wierzę widać ... 10, null - mylące. Następujące byłoby powtarzalne:

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

Jeśli potrzebujesz, możesz oczywiście napisać metodę wielokrotnego użytku, która miała lambda.

Powiązane problemy