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.