wake-up-neo.com

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

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 es oder warum muss es geändert werden? implicitly captured closure: end, start

286
PiousVenom

Die Warnung besagt, dass die Variablen end und start am 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 erhalte ich die Warnung "Implizit erfasster Verschluss: g". Es sagt mir, dass g nicht Müll gesammelt sein 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.

In meinem Beispiel werden g und i zur Ausführung meiner Delegierten in derselben Klasse abgelegt. Wenn es sich bei g um ein schweres Objekt mit vielen verbleibenden Ressourcen handelt, 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 Da in C # die anonymen Methoden immer in einer Klasse pro Methode gespeichert werden, gibt es zwei Möglichkeiten, dies zu vermeiden:

  1. Verwenden Sie eine Instanzmethode anstelle einer anonymen.

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

385
Console

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 wie folgt:

[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 Capture Methode wird 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));
}

Das zweite Lambda verwendet zwar nicht x, es kann jedoch kein Müll gesammelt werden, da x als Eigenschaft der im Lambda verwendeten generierten Klasse kompiliert wird.

32
Smartkid

Die Warnung ist gültig und wird in Methoden angezeigt, die mehr als ein Lambda und verschiedene Werte erfassen haben.

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 von any dieser Lambdas erfassten Werte darstellen

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 (etwas 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, dass die Instanz von LambdaHelper sowohl p1 Als auch p2 Speichert.

Stell dir das vor:

  • callable1 Behält einen langlebigen Verweis auf sein Argument bei, helper.Lambda1
  • callable2 Behält keinen Verweis auf sein Argument bei, helper.Lambda2

In diesem Fall verweist der Verweis auf helper.Lambda1 Auch indirekt auf die Zeichenfolge in p2. 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 benötigt, was sich auf die GC auswirken kann, wenn sie von gen0 nach gen1 befördert werden.

28
Drew Noakes

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 (d. H. Über .ToList ()) innerhalb der Methode aktualisieren, um GC für die im L2S-Lambda erfassten Instanzvariablen der Methode zu ermöglichen.

3
Jason Dufair

Sie können die Gründe für R # -Vorschläge jederzeit herausfinden, indem Sie auf die folgenden Hinweise klicken:

enter image description here

Dieser Hinweis wird Sie leiten hier .


Diese Prüfung macht Sie darauf aufmerksam, dass mehr Verschlusswerte erfasst werden, als offensichtlich sichtbar sind, was sich auf die Lebensdauer dieser Werte auswirkt.

Betrachten Sie den folgenden Code:

using System; öffentliche Klasse 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);
    };
} } In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For

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, die die erfassten Werte enthalten, und Methoden, die den Abschluss selbst darstellen, umgeschrieben. Der C # -Compiler erstellt nur eine solche private Klasse pro Methode. Wenn in einer Methode mehr als eine Closure definiert ist, enthält diese Klasse mehrere Methoden, eine für jede Closure, sowie alle erfassten Werte aus allen Closures.

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 versiegelte Klasse <> c__DisplayClass1_0 {public object obj1; öffentliches Objekt 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);
} } When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn't used

in einem der Verschlüsse wird es noch eingefangen. Dies ist die "implizite" Erfassung, die ReSharper hervorhebt.

Die Folge dieser Überprüfung ist, dass der implizit erfasste Schließungswert erst dann als Müll eingesammelt wird, wenn der Schließungswert selbst als Müll eingesammelt wird. Die Lebensdauer dieses Werts ist jetzt an die Lebensdauer eines Abschlusses gebunden, der den Wert nicht explizit verwendet. Wenn die Schließung von langer Dauer 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, es jedoch für Versionen und Implementierungen wie Microsoft (vor und nach Roslyn) oder Monos Compiler konsistent ist. Die Implementierung muss wie beschrieben funktionieren, damit mehrere Abschlüsse, die einen Werttyp erfassen, korrekt verarbeitet werden können. Wenn beispielsweise mehrere Closures 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 nun die maximale Lebensdauer eines Verschlusses ist, der einen der Werte erfasst.

2
anatol