Warum sagt mir ReSharper "implizit erfasste Schließung"?

296

Ich habe folgenden Code:

public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
    Log("Calculating Daily Pull Force Max...");

    var pullForceList = start == null
                             ? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
                             : _pullForce.Where(
                                 (t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 && 
                                           DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();

    _pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);

    return _pullForceDailyMax;
}

Jetzt habe ich einen Kommentar zu der Zeile hinzugefügt, in der ReSharper eine Änderung vorschlägt. Was bedeutet das oder warum muss es geändert werden?implicitly captured closure: end, start

PiousVenom
quelle
6
MyCodeSucks korrigiert bitte die akzeptierte Antwort: Die Antwort von kevingessner ist falsch (wie in den Kommentaren erläutert), und wenn sie als akzeptiert markiert wird, werden Benutzer irregeführt, wenn sie die Antwort von Console nicht bemerken.
Albireo
1
Sie können dies auch sehen, wenn Sie Ihre Liste außerhalb eines Try / Catch definieren und alles in Try / Catch hinzufügen und dann die Ergebnisse auf ein anderes Objekt setzen. Durch Verschieben des Definierens / Hinzufügens innerhalb von try / catch wird GC ermöglicht. Hoffentlich macht das Sinn.
Micah Montoya

Antworten:

391

Die Warnung sagt Ihnen, dass die Variablen endund startam Leben bleiben, da alle Lambdas in dieser Methode am Leben bleiben.

Schauen Sie sich das kurze Beispiel an

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    int i = 0;
    Random g = new Random();
    this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
    this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}

Beim ersten Lambda wird die Warnung "Implizit erfasste Schließung: g" angezeigt. Es sagt mir, dass gkein Müll gesammelt werden kann, solange das erste Lambda verwendet wird.

Der Compiler generiert eine Klasse für beide Lambda-Ausdrücke und fügt alle Variablen in diese Klasse ein, die in den Lambda-Ausdrücken verwendet werden.

Also in meinem Beispiel gund iwerden in der gleichen Klasse für die Ausführung meiner Delegierten gehalten. Wenn ges sich um ein schweres Objekt handelt, bei dem viele Ressourcen zurückbleiben, konnte der Garbage Collector es nicht zurückfordern, da die Referenz in dieser Klasse noch aktiv ist, solange einer der Lambda-Ausdrücke verwendet wird. Dies ist also ein potenzieller Speicherverlust, und das ist der Grund für die R # -Warnung.

@splintor Wie in C # werden die anonymen Methoden immer in einer Klasse pro Methode gespeichert. Es gibt zwei Möglichkeiten, dies zu vermeiden:

  1. Verwenden Sie eine Instanzmethode anstelle einer anonymen.

  2. Teilen Sie die Erstellung der Lambda-Ausdrücke in zwei Methoden auf.

Konsole
quelle
30
Wie kann diese Erfassung vermieden werden?
Schiene
2
Vielen Dank für diese großartige Antwort. Ich habe erfahren, dass es einen Grund gibt, eine nicht anonyme Methode zu verwenden, auch wenn sie nur an einer Stelle verwendet wird.
ScottRhee
1
@splintor Instanziieren Sie das Objekt innerhalb des Delegaten oder übergeben Sie es stattdessen als Parameter. Im obigen Fall besteht das gewünschte Verhalten, soweit ich das beurteilen kann, darin, einen Verweis auf die RandomInstanz zu enthalten.
Casey
2
@emodendroket Richtig, an dieser Stelle sprechen wir über Codestil und Lesbarkeit. Ein Feld ist leichter zu überlegen. Wenn Speicherdruck oder Objektlebensdauer wichtig sind, würde ich das Feld auswählen, andernfalls würde ich es in der prägnanteren Schließung belassen.
Yzorg
1
Mein Fall (stark) vereinfacht auf eine Fabrikmethode reduziert, die einen Foo und einen Balken erzeugt. Anschließend wird das Erfassen von Lambas für Ereignisse abonniert, die von diesen beiden Objekten ausgesetzt wurden, und überraschenderweise hält der Foo die Erfassungen von der Lamba des Bar-Ereignisses am Leben und umgekehrt. Ich komme aus C ++, wo dieser Ansatz gut funktioniert hätte, und war mehr als ein wenig erstaunt, dass die Regeln hier anders waren. Je mehr Sie wissen, denke ich.
dlf
35

Einverstanden mit Peter Mortensen.

Der C # -Compiler generiert nur einen Typ, der alle Variablen für alle Lambda-Ausdrücke in einer Methode kapselt.

Zum Beispiel angesichts des Quellcodes:

public class ValueStore
{
    public Object GetValue()
    {
        return 1;
    }

    public void SetValue(Object obj)
    {
    }
}

public class ImplicitCaptureClosure
{
    public void Captured()
    {
        var x = new object();

        ValueStore store = new ValueStore();
        Action action = () => store.SetValue(x);
        Func<Object> f = () => store.GetValue();    //Implicitly capture closure: x
    }
}

Der Compiler generiert einen Typ, der wie folgt aussieht:

[CompilerGenerated]
private sealed class c__DisplayClass2
{
  public object x;
  public ValueStore store;

  public c__DisplayClass2()
  {
    base.ctor();
  }

  //Represents the first lambda expression: () => store.SetValue(x)
  public void Capturedb__0()
  {
    this.store.SetValue(this.x);
  }

  //Represents the second lambda expression: () => store.GetValue()
  public object Capturedb__1()
  {
    return this.store.GetValue();
  }
}

Und die CaptureMethode ist kompiliert als:

public void Captured()
{
  ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
  cDisplayClass2.x = new object();
  cDisplayClass2.store = new ValueStore();
  Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
  Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}

Obwohl das zweite Lambda nicht verwendet wird x, kann es nicht als Müll gesammelt werden, da xes als Eigenschaft der generierten Klasse kompiliert wird, die im Lambda verwendet wird.

Smartkid
quelle
31

Die Warnung ist gültig und wird in Methoden mit mehr als einem Lambda angezeigt. Sie erfassen unterschiedliche Werte .

Wenn eine Methode aufgerufen wird, die Lambdas enthält, wird ein vom Compiler generiertes Objekt instanziiert mit:

  • Instanzmethoden, die die Lambdas darstellen
  • Felder, die alle Werte darstellen, die von einem dieser Lambdas erfasst wurden

Als Beispiel:

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var p1 = 1;
        var p2 = "hello";

        callable1(() => p1++);    // WARNING: Implicitly captured closure: p2

        callable2(() => { p2.ToString(); p1++; });
    }
}

Untersuchen Sie den generierten Code für diese Klasse (ein wenig aufgeräumt):

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var helper = new LambdaHelper();

        helper.p1 = 1;
        helper.p2 = "hello";

        callable1(helper.Lambda1);
        callable2(helper.Lambda2);
    }

    [CompilerGenerated]
    private sealed class LambdaHelper
    {
        public int p1;
        public string p2;

        public void Lambda1() { ++p1; }

        public void Lambda2() { p2.ToString(); ++p1; }
    }
}

Beachten Sie die Instanz der LambdaHelpererstellten Speicher sowohl p1als auch p2.

Stell dir das vor:

  • callable1 hält einen langlebigen Verweis auf seine Argumentation, helper.Lambda1
  • callable2 behält keinen Verweis auf sein Argument, helper.Lambda2

In dieser Situation verweist der Verweis auf helper.Lambda1auch indirekt auf die Zeichenfolge in p2, und dies bedeutet, dass der Garbage Collector die Zuordnung nicht aufheben kann. Im schlimmsten Fall handelt es sich um ein Speicher- / Ressourcenleck. Alternativ können Objekte länger am Leben bleiben als sonst erforderlich, was sich auf die GC auswirken kann, wenn sie von gen0 zu gen1 befördert werden.

Drew Noakes
quelle
Wenn wir den Verweis p1von callable2so herausnehmen würden: callable2(() => { p2.ToString(); });- Würde dies immer noch nicht das gleiche Problem verursachen (Garbage Collector kann es nicht freigeben), LambdaHelperdas noch p1und enthält p2?
Antony
1
Ja, das gleiche Problem würde bestehen. Der Compiler erstellt ein Erfassungsobjekt (dh LambdaHelperoben) für alle Lambdas innerhalb der übergeordneten Methode. Selbst wenn callable2es nicht verwendet p1würde, würde es dasselbe Erfassungsobjekt wie verwenden callable1, und dieses Erfassungsobjekt würde sowohl auf p1als auch verweisen p2. Beachten Sie, dass dies nur für Referenztypen wirklich wichtig ist und p1in diesem Beispiel ein Werttyp ist.
Drew Noakes
3

Bei Linq to Sql-Abfragen wird möglicherweise diese Warnung angezeigt. Der Gültigkeitsbereich des Lambda kann die Methode überleben, da die Abfrage häufig aktualisiert wird, nachdem die Methode außerhalb des Gültigkeitsbereichs liegt. Abhängig von Ihrer Situation möchten Sie möglicherweise die Ergebnisse (dh über .ToList ()) innerhalb der Methode aktualisieren, um eine GC für die im L2S-Lambda erfassten Instanzvariablen der Methode zu ermöglichen.

Jason Dufair
quelle
2

Sie können immer mit Gründen für R # -Vorschläge herausfinden, indem Sie einfach auf die unten gezeigten Hinweise klicken:

Geben Sie hier die Bildbeschreibung ein

Dieser Hinweis leitet Sie hierher .


Diese Inspektion macht Sie darauf aufmerksam, dass mehr Schließwerte erfasst werden, als offensichtlich sichtbar ist, was sich auf die Lebensdauer dieser Werte auswirkt.

Betrachten Sie den folgenden Code:

using System; 
public class Class1 {
    private Action _someAction;

    public void Method() {
        var obj1 = new object();
        var obj2 = new object();

        _someAction += () => {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        };

        // "Implicitly captured closure: obj2"
        _someAction += () => {
            Console.WriteLine(obj1);
        };
    }
}

Im ersten Abschluss sehen wir, dass sowohl obj1 als auch obj2 explizit erfasst werden. Wir können dies sehen, indem wir uns den Code ansehen. Beim zweiten Abschluss können wir sehen, dass obj1 explizit erfasst wird, aber ReSharper warnt uns, dass obj2 implizit erfasst wird.

Dies liegt an einem Implementierungsdetail im C # -Compiler. Während der Kompilierung werden Abschlüsse in Klassen mit Feldern umgeschrieben, die die erfassten Werte enthalten, und Methoden, die den Abschluss selbst darstellen. Der C # -Compiler erstellt nur eine solche private Klasse pro Methode. Wenn in einer Methode mehr als ein Abschluss definiert ist, enthält diese Klasse mehrere Methoden, eine für jeden Abschluss, und enthält auch alle erfassten Werte aus allen Abschlüssen.

Wenn wir uns den Code ansehen, den der Compiler generiert, sieht er ungefähr so ​​aus (einige Namen wurden bereinigt, um das Lesen zu erleichtern):

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object obj2;

        internal void <Method>b__0()
        {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        }

        internal void <Method>b__1()
        {
            Console.WriteLine(obj1);
        }
    }

    private Action _someAction;

    public void Method()
    {
        // Create the display class - just one class for both closures
        var dc = new Class1.<>c__DisplayClass1_0();

        // Capture the closure values as fields on the display class
        dc.obj1 = new object();
        dc.obj2 = new object();

        // Add the display class methods as closure values
        _someAction += new Action(dc.<Method>b__0);
        _someAction += new Action(dc.<Method>b__1);
    }
}

Wenn die Methode ausgeführt wird, wird die Anzeigeklasse erstellt, die alle Werte für alle Abschlüsse erfasst. Selbst wenn ein Wert in einem der Abschlüsse nicht verwendet wird, wird er dennoch erfasst. Dies ist die "implizite" Erfassung, die ReSharper hervorhebt.

Diese Inspektion impliziert, dass der implizit erfasste Verschlusswert erst dann als Müll gesammelt wird, wenn der Verschluss selbst Müll ist. Die Lebensdauer dieses Werts ist jetzt an die Lebensdauer eines Abschlusses gebunden, der den Wert nicht explizit verwendet. Wenn der Abschluss langlebig ist, kann sich dies negativ auf Ihren Code auswirken, insbesondere wenn der erfasste Wert sehr groß ist.

Beachten Sie, dass dies zwar ein Implementierungsdetail des Compilers ist, jedoch über Versionen und Implementierungen wie Microsoft (vor und nach Roslyn) oder Monos Compiler hinweg konsistent ist. Die Implementierung muss wie beschrieben funktionieren, um mehrere Abschlüsse, die einen Werttyp erfassen, korrekt zu behandeln. Wenn beispielsweise mehrere Abschlüsse ein int erfassen, müssen sie dieselbe Instanz erfassen, was nur mit einer einzelnen gemeinsam genutzten privaten verschachtelten Klasse möglich ist. Der Nebeneffekt davon ist, dass die Lebensdauer aller erfassten Werte jetzt die maximale Lebensdauer eines Abschlusses ist, der einen der Werte erfasst.

Anatol
quelle