Zugriff auf jede Variable in der Schließungswarnung

85

Ich erhalte die folgende Warnung:

Zugriff auf jede Variable im Abschluss. Kann sich beim Kompilieren mit verschiedenen Compilerversionen unterschiedlich verhalten.

So sieht es in meinem Editor aus:

oben genannte Fehlermeldung in einem Hover-Popup

Ich weiß, wie diese Warnung behoben werden kann, aber ich möchte wissen, warum ich diese Warnung erhalten sollte.

Geht es um die "CLR" -Version? Hat es mit "IL" zu tun?

Jeroen
quelle
1
TL; DR Antwort: Fügen Sie am Ende Ihres Abfrageausdrucks .ToList () oder .ToArray () hinzu, und die Warnung wird entfernt
JoelFan

Antworten:

135

Diese Warnung besteht aus zwei Teilen. Das erste ist...

Zugriff auf jede Variable im Abschluss

... was an sich nicht ungültig ist, aber auf den ersten Blick kontraintuitiv. Es ist auch sehr schwer, es richtig zu machen. (So ​​sehr, dass der Artikel, auf den ich unten verweise, dies als "schädlich" beschreibt.)

Nehmen Sie Ihre Anfrage entgegen und stellen Sie fest, dass der Code, den Sie extrahiert haben, im Grunde eine erweiterte Form dessen ist, was der C # -Compiler (vor C # 5) für foreach1 generiert :

Ich verstehe nicht, warum Folgendes nicht gültig ist:

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

Nun, es ist syntaktisch gültig. Und wenn Sie in Ihrer Schleife nur den Wert von verwenden, sist alles gut. Das Schließen sführt jedoch zu einem kontraintuitiven Verhalten. Schauen Sie sich den folgenden Code an:

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

Wenn Sie diesen Code ausführen, erhalten Sie die folgende Konsolenausgabe:

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

Das erwarten Sie.

Führen Sie den folgenden Code unmittelbar nach dem obigen Code aus, um etwas zu sehen, das Sie wahrscheinlich nicht erwarten :

foreach (var action in countingActions)
    action();

Sie erhalten die folgende Konsolenausgabe:

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

Warum? Weil wir fünf Funktionen erstellt haben, die alle genau dasselbe tun: Drucken Sie den Wert von s(den wir geschlossen haben). In Wirklichkeit haben sie dieselbe Funktion ("Drucken s", "Drucken s", "Drucken s" ...).

An dem Punkt, an dem wir sie verwenden, tun sie genau das, was wir verlangen: Drucken Sie den Wert von aus s. Wenn Sie sich den letzten bekannten Wert von ansehen s, werden Sie sehen, dass dies der Fall ist 5. Also werden wir s == 5fünfmal auf die Konsole gedruckt.

Welches ist genau das, wonach wir gefragt haben, aber wahrscheinlich nicht das, was wir wollen.

Der zweite Teil der Warnung ...

Kann sich beim Kompilieren mit verschiedenen Compilerversionen unterschiedlich verhalten.

...es ist was es ist. Beginnend mit C # 5 generiert der Compiler einen anderen Code, der dies "verhindert"foreach .

Daher führt der folgende Code unter verschiedenen Versionen des Compilers zu unterschiedlichen Ergebnissen:

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

Folglich wird auch die R # -Warnung ausgegeben :)

Mein erstes Code-Snippet oben zeigt in allen Versionen des Compilers das gleiche Verhalten, da ich es nicht verwende foreach(vielmehr habe ich es so erweitert, wie es Compiler vor C # 5 tun).

Ist das für die CLR-Version?

Ich bin mir nicht ganz sicher, was Sie hier fragen.

Eric Lipperts Beitrag besagt, dass die Änderung "in C # 5" erfolgt. SoVermutlich müssen Sie auf .NET 4.5 oder höher abzielen mit einem C # 5 oder höher Compiler, um das neue Verhalten zu erhalten, und alles davor bekommt das alte Verhalten.

Es ist jedoch eine Funktion des Compilers und nicht der .NET Framework-Version.

Gibt es Relevanz für IL?

Unterschiedlicher Code erzeugt unterschiedliche IL, sodass in diesem Sinne Konsequenzen für die generierte IL entstehen.

1 foreach ist ein viel häufigeres Konstrukt als der Code, den Sie in Ihrem Kommentar gepostet haben. Das Problem tritt normalerweise durch die Verwendung von foreachund nicht durch manuelle Aufzählung auf. Aus diesem Grund verhindern die Änderungen foreachin C # 5 dieses Problem, jedoch nicht vollständig.

ta.speot.is
quelle
7
Ich habe tatsächlich die foreach-Schleife auf verschiedenen Compilern ausprobiert, um mit demselben Ziel (.Net 3.5) unterschiedliche Ergebnisse zu erzielen. Ich habe VS2010 (der wiederum den mit .net 4.0 verknüpften Compiler verwendet, glaube ich) und VS2012 (.net 4.5-Compiler, glaube ich) verwendet. Im Prinzip bedeutet dies, dass Sie, wenn Sie VS2013 verwenden und ein Projekt mit dem Ziel .NET 3.5 bearbeiten und es auf einem Build-Server erstellen, auf dem ein etwas älteres Framework installiert ist, möglicherweise andere Ergebnisse Ihres Programms auf Ihrem Computer als im bereitgestellten Build sehen.
Ykok
Gute Antwort, aber nicht sicher, wie "foreach" relevant ist. Würde dies nicht mit einer manuellen Aufzählung oder sogar einer einfachen for-Schleife (int i = 0; i <collection.Size; i ++) geschehen? Es scheint ein Problem mit Verschlüssen zu sein, die außerhalb des Bereichs liegen, oder genauer gesagt, ein Problem mit Menschen, die verstehen, wie sich Verschlüsse verhalten, wenn sie den Bereich verlassen, in dem sie definiert wurden.
Brad
Das foreachZeug hier kommt aus dem Inhalt der Frage. Sie haben Recht, dass dies auf verschiedene, allgemeinere Arten geschehen kann.
ta.speot.is
1
Warum warnt mich R # immer noch, liest es nicht das Ziel-Framework, das ich auf 4.5 gesetzt habe?
Johnny_D
1
"Vermutlich müssen Sie also auf .NET 4.5 oder höher abzielen" Diese Aussage ist nicht wahr. Die Version von .NET, auf die Sie abzielen, hat keine Auswirkungen darauf. Das Verhalten wird auch in .NET 2.0, 3.5 und 4 geändert, wenn Sie zum Kompilieren C # 5 (VS 2012 oder neuer) verwenden. Aus diesem Grund wird diese Warnung nur in .NET 4.0 oder früher angezeigt. Wenn Sie auf 4.5 abzielen, wird die Warnung nicht angezeigt, da Sie 4.5 auf einem C # 4-Compiler oder einem älteren Compiler nicht kompilieren können.
Scott Chamberlain
12

Die erste Antwort ist großartig, also dachte ich, ich würde nur eine Sache hinzufügen.

Sie erhalten die Warnung, weil in Ihrem Beispielcode ReflectedModel eine IEnumerable zugewiesen wird, die nur zum Zeitpunkt der Aufzählung ausgewertet wird, und die Aufzählung selbst außerhalb der Schleife erfolgen kann, wenn Sie ReflectModel etwas mit einem breiteren Bereich zugewiesen haben .

Wenn du dich verändert hast

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

zu

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

Dann würde ReflectedModel eine bestimmte Liste innerhalb der foreach-Schleife zugewiesen, sodass Sie die Warnung nicht erhalten würden, da die Aufzählung definitiv innerhalb der Schleife und nicht außerhalb der Schleife erfolgen würde.

David
quelle
Ich habe viele wirklich lange Erklärungen gelesen, die dieses Problem für mich nicht gelöst haben, und dann eine kurze, die es getan hat. Vielen Dank!
Charles Clayton
Ich las die akzeptierte Antwort und dachte nur: "Wie ist es ein Abschluss, wenn er die Variablen nicht bindet?" aber jetzt verstehe ich, dass es darum geht, wann die Bewertung stattfindet, danke!
Jerome
Ja, dies ist eine offensichtliche universelle Lösung. Langsam, speicherintensiv, aber ich denke, es funktioniert wirklich zu 100% in allen Fällen.
Al Kepp
8

Eine Variable mit Blockbereich sollte die Warnung auflösen.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Dmitry Gogol
quelle