Dostęp do każdej zmiennej w ostrzeżeniu o zamknięciu


86

Otrzymuję następujące ostrzeżenie:

Dostęp do każdej zmiennej w zamknięciu. Może mieć różne zachowanie po skompilowaniu z różnymi wersjami kompilatora.

Tak to wygląda w moim edytorze:

wyżej wymieniony komunikat o błędzie w wyskakującym okienku

Wiem, jak naprawić to ostrzeżenie, ale chcę wiedzieć, dlaczego otrzymuję to ostrzeżenie?

Czy chodzi o wersję „CLR”? Czy jest to związane z „IL”?



1
Odpowiedź TL; DR: dodaj .ToList () lub .ToArray () na końcu wyrażenia zapytania, a pozbędziesz się ostrzeżenia
JoelFan

Odpowiedzi:


136

Ostrzeżenie to składa się z dwóch części. Pierwsza to ...

Dostęp do każdej zmiennej w zamknięciu

... co samo w sobie nie jest nieważne, ale na pierwszy rzut oka jest sprzeczne z intuicją. Bardzo trudno jest też zrobić to dobrze. (Do tego stopnia, że ​​artykuł, do którego odsyłam poniżej, opisuje to jako „szkodliwe”).

Weź swoje zapytanie, zauważając, że kod, który wyciągnąłeś, jest w zasadzie rozszerzoną formą tego, co kompilator C # (przed C # 5) generuje dla foreach1 :

Nie rozumiem, dlaczego [poniższe stwierdzenie jest] nieprawidłowe:

string s; while (enumerator.MoveNext()) { s = enumerator.Current; ...

Cóż, jest poprawna składniowo. A jeśli wszystko robisz w pętli przy użyciu wartości z sto wszystko jest dobre. Ale zamknięcie sdoprowadzi do zachowania sprzecznego z intuicją. Spójrz na następujący kod:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

    while (enumerator.MoveNext())
    {
        s = enumerator.Current;

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Jeśli uruchomisz ten kod, otrzymasz następujące dane wyjściowe konsoli:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Tego oczekujesz.

Aby zobaczyć coś, czego prawdopodobnie się nie spodziewasz, uruchom następujący kod bezpośrednio po powyższym kodzie:

foreach (var action in countingActions)
    action();

Otrzymasz następujące dane wyjściowe konsoli:

s == 5
s == 5
s == 5
s == 5
s == 5

Czemu? Ponieważ stworzyliśmy pięć funkcji, które wszystkie robią dokładnie to samo: wypisuje wartość s(którą zamknęliśmy). W rzeczywistości pełnią tę samą funkcję („Drukuj s”, „Drukuj s”, „Drukujs ” ...).

W momencie, w którym zamierzamy ich użyć, robią dokładnie to, o co prosimy: wypisują wartość s. Jeśli spojrzysz na ostatnią znaną wartość s, zobaczysz, że to jest 5. Więc s == 5pięć razy zostajemy wydrukowani na konsoli.

To jest dokładnie to, o co prosiliśmy, ale prawdopodobnie nie to, czego chcemy.

Druga część ostrzeżenia ...

Może mieć różne zachowanie po skompilowaniu z różnymi wersjami kompilatora.

... jest tym, czym jest. Począwszy od C # 5, kompilator generuje inny kod, który „zapobiega” temu zjawisku za pośrednictwemforeach .

Dlatego poniższy kod da różne wyniki w różnych wersjach kompilatora:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

W konsekwencji wygeneruje również ostrzeżenie R # :)

Mój pierwszy fragment kodu, powyżej, będzie wykazywał to samo zachowanie we wszystkich wersjach kompilatora, ponieważ nie używam foreach(raczej rozszerzyłem go tak, jak robią to kompilatory sprzed C # 5).

Czy to dotyczy wersji CLR?

Nie jestem pewien, o co tutaj pytasz.

Post Erica Lipperta mówi, że zmiana zachodzi „w C # 5”. Więcprawdopodobnie musisz kierować reklamy na .NET 4.5 lub nowszy z kompilatorem C # 5 lub nowszym, aby uzyskać nowe zachowanie, a wszystko, co było wcześniej, otrzymuje stare zachowanie.

Ale żeby było jasne, jest to funkcja kompilatora, a nie wersja .NET Framework.

Czy istnieje związek z IL?

Inny kod tworzy różne IL, więc w tym sensie istnieją konsekwencje dla wygenerowanego IL.

1 foreach jest znacznie bardziej powszechną konstrukcją niż kod, który opublikowałeś w komentarzu. Problem zwykle pojawia się w wyniku użycia foreach, a nie ręcznego wyliczania. Dlatego zmiany foreachw języku C # 5 pomagają zapobiec temu problemowi, ale nie całkowicie.


7
W rzeczywistości wypróbowałem pętlę foreach na różnych kompilatorach, uzyskując różne wyniki przy użyciu tego samego celu (.Net 3.5). Użyłem VS2010 (który z kolei używa kompilatora związanego z .net 4.0, jak sądzę) i VS2012 (uważam, że kompilator .net 4.5). Zasadniczo oznacza to, że jeśli używasz VS2013 i edytujesz projekt przeznaczony dla .Net 3.5 i budujesz go na serwerze kompilacji z zainstalowaną nieco starszą strukturą, możesz zobaczyć inne wyniki programu na swoim komputerze niż wdrożona kompilacja.
Ykok

Dobra odpowiedź, ale nie wiem, jakie znaczenie ma wyrażenie „foreach”. Czy nie stałoby się tak w przypadku ręcznego wyliczania, czy nawet prostej pętli for (int i = 0; i <collection.Size; i ++)? Wydaje się, że jest to problem z zamknięciami wychodzącymi poza zakres, a dokładniej, problem ze zrozumieniem przez ludzi, jak zachowują się zamknięcia, gdy wychodzą poza zakres, w którym zostały zdefiniowane.
Brad,

Treść foreachtutaj pochodzi z treści pytania. Masz rację, że może się to zdarzyć na różne, bardziej ogólne sposoby.
ta.speot.is

1
Dlaczego R # wciąż mnie ostrzega, czy nie czyta frameworka docelowego, który ustawiłem na 4.5.
Johnny_D

1
„Więc prawdopodobnie musisz kierować reklamy na .NET 4.5 lub nowszy” To stwierdzenie nie jest prawdą. Wersja docelowa platformy .NET nie ma na to wpływu, zachowanie jest również zmieniane w .NET 2.0, 3.5 i 4, jeśli do kompilacji używasz języka C # 5 (VS 2012 lub nowszego). Dlatego to ostrzeżenie pojawia się tylko na platformie .NET 4.0 lub starszej, jeśli docelowo jest to 4.5, nie otrzymasz ostrzeżenia, ponieważ nie możesz skompilować 4.5 na kompilatorze C # 4 lub starszym.
Scott Chamberlain

12

Pierwsza odpowiedź jest świetna, więc pomyślałem, że dodam tylko jedną rzecz.

Otrzymujesz ostrzeżenie, ponieważ w Twoim przykładowym kodzie odzwierciedlonyModel jest przypisywany element IEnumerable, który będzie oceniany tylko w czasie wyliczania, a samo wyliczenie może się zdarzyć poza pętlą, jeśli przypisano odbicieModel do czegoś o szerszym zakresie .

Jeśli się zmieniłeś

...Where(x => x.Name == property.Value)

do

...Where(x => x.Name == property.Value).ToList()

następnie ReflektorModel otrzyma określoną listę w pętli foreach, więc nie otrzymasz ostrzeżenia, ponieważ wyliczenie z pewnością nastąpi w pętli, a nie poza nią.


Przeczytałem wiele naprawdę długich wyjaśnień, które nie rozwiązały tego problemu, a potem jedno krótkie, które rozwiązało. dzięki!
Charles Clayton,

Przeczytałem zaakceptowaną odpowiedź i pomyślałem „jak to jest zamknięcie, jeśli nie wiąże zmiennych?” ale teraz rozumiem, że chodzi o ocenę, dzięki!
Jerome

Tak, to oczywiste, uniwersalne rozwiązanie. Powolny, wymagający dużej ilości pamięci, ale myślę, że to naprawdę działa w 100% we wszystkich przypadkach.
Al Kepp

8

Zmienna o zasięgu blokowym powinna rozwiązać ostrzeżenie.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.