Perché ReSharper mi dice "chiusura implicitamente catturata"?


296

Ho il codice seguente:

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

Ora, ho aggiunto un commento sulla linea secondo cui ReSharper sta suggerendo una modifica. Cosa significa o perché dovrebbe essere cambiato?implicitly captured closure: end, start


6
MyCodeSucks correggi la risposta accettata: quella di kevingessner è errata (come spiegato nei commenti) e averla contrassegnata come accettata fuorvia gli utenti se non notano la risposta della console.
Albireo,

1
Potresti anche vedere questo se definisci la tua lista al di fuori di un tentativo / cattura e fai tutte le tue aggiunte nel tentativo / cattura e poi imposti i risultati su un altro oggetto. Spostare la definizione / aggiunta all'interno di try / catch consentirà GC. Speriamo che abbia senso.
Micah Montoya,

Risposte:


391

L'avvertimento ti dice che le variabili ende startrimangono in vita come qualsiasi lambda all'interno di questo metodo rimane in vita.

Dai un'occhiata al breve esempio

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

Ricevo un avviso "Chiusura implicitamente catturata: g" alla prima lambda. Mi sta dicendo che gnon è possibile raccogliere l'immondizia fintanto che la prima lambda è in uso.

Il compilatore genera una classe per entrambe le espressioni lambda e inserisce tutte le variabili in quella classe che vengono utilizzate nelle espressioni lambda.

Quindi nel mio esempio ge isono tenuti nella stessa classe per l'esecuzione dei miei delegati. Se gè un oggetto pesante con molte risorse rimaste, il Garbage Collector non è in grado di recuperarlo, perché il riferimento in questa classe è ancora in vita fintanto che una delle espressioni lambda è in uso. Quindi questa è una potenziale perdita di memoria e questa è la ragione per l'avvertimento R #.

@splintor Come in C # i metodi anonimi sono sempre archiviati in una classe per metodo ci sono due modi per evitarlo:

  1. Utilizzare un metodo di istanza anziché anonimo.

  2. Dividi la creazione delle espressioni lambda in due metodi.


30
Quali sono i modi possibili per evitare questa acquisizione?
splintore

2
Grazie per questa fantastica risposta: ho imparato che esiste un motivo per utilizzare un metodo non anonimo anche se utilizzato in un solo posto.
ScottRhee,

1
@splintor Crea un'istanza dell'oggetto all'interno del delegato o passalo invece come parametro. Nel caso sopra, per quanto posso dire, il comportamento desiderato è in realtà tenere un riferimento Randomall'istanza, però.
Casey,

2
@emodendroket Corretto, a questo punto stiamo parlando di stile e leggibilità del codice. Un campo è più facile da ragionare. Se la pressione della memoria o la durata degli oggetti sono importanti, sceglierei il campo, altrimenti lo lascerei nella chiusura più concisa.
yzorg,

1
Il mio caso (fortemente) semplificato si è ridotto a un metodo di fabbrica che crea un Foo e un Bar. Quindi si iscrive a catturare lambas agli eventi esposti da quei due oggetti e, sorpresa sorpresa, il Foo mantiene in vita e viceversa le catture della lamba dell'evento Bar. Vengo dal C ++ in cui questo approccio avrebbe funzionato perfettamente, ed è stato più che un po 'stupito di scoprire che qui le regole erano diverse. Più sai, suppongo.
dlf

35

Concordato con Peter Mortensen.

Il compilatore C # genera un solo tipo che incapsula tutte le variabili per tutte le espressioni lambda in un metodo.

Ad esempio, dato il codice sorgente:

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

Il compilatore genera un tipo simile al seguente:

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

E il Capturemetodo è compilato come:

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

Sebbene la seconda lambda non utilizzi x, non può essere garbage collection poiché xviene compilata come proprietà della classe generata utilizzata nella lambda.


31

L'avviso è valido e visualizzato in metodi che hanno più di una lambda e acquisiscono valori diversi .

Quando viene invocato un metodo che contiene lambdas, viene istanziato un oggetto generato dal compilatore con:

  • metodi di istanza che rappresentano i lambda
  • campi che rappresentano tutti i valori acquisiti da uno di questi lambda

Come esempio:

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

Esamina il codice generato per questa classe (riordinato un po '):

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

Nota l'istanza dei LambdaHelpernegozi creati sia p1e p2.

Immaginalo:

  • callable1 mantiene un riferimento di lunga durata al suo argomento, helper.Lambda1
  • callable2 non mantiene un riferimento al suo argomento, helper.Lambda2

In questa situazione, il riferimento helper.Lambda1fa riferimento indirettamente anche alla stringa p2e ciò significa che il Garbage Collector non sarà in grado di deallocare. Nel peggiore dei casi è una perdita di memoria / risorsa. In alternativa, può mantenere in vita gli oggetti più a lungo del necessario, il che può avere un impatto su GC se vengono promossi da gen0 a gen1.


se eliminassimo il riferimento di p1in callable2questo modo: callable2(() => { p2.ToString(); });- ciò non causerebbe ancora lo stesso problema (il garbage collector non sarà in grado di deallocare) poiché LambdaHelperconterrà ancora p1e p2?
Antony,

1
Sì, lo stesso problema esisterebbe. Il compilatore crea un oggetto di acquisizione (cioè LambdaHelpersopra) per tutte le lambda all'interno del metodo parent. Quindi, anche se callable2non utilizzato p1, condividerebbe lo stesso oggetto di acquisizione callable1e quell'oggetto di acquisizione farebbe riferimento sia a p1che a p2. Si noti che questo è importante solo per i tipi di riferimento e p1in questo esempio è un tipo di valore.
Drew Noakes,

3

Per le query da Linq a Sql, potresti ricevere questo avviso. L'ambito del lambda può sopravvivere al metodo a causa del fatto che la query viene spesso attualizzata dopo che il metodo è fuori dall'ambito. A seconda della situazione, è possibile che si desideri attualizzare i risultati (ad es. Tramite .ToList ()) all'interno del metodo per consentire GC sui parametri di istanza del metodo acquisiti nella lambda L2S.


2

Puoi sempre capire con una ragione di suggerimenti R # semplicemente facendo clic sui suggerimenti come mostrato di seguito:

inserisci qui la descrizione dell'immagine

Questo suggerimento ti indirizzerà qui .


Questa ispezione attira la vostra attenzione sul fatto che vengono acquisiti più valori di chiusura di quelli che sono ovviamente visibili, il che ha un impatto sulla durata di questi valori.

Considera il seguente codice:

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

Nella prima chiusura, vediamo che sia obj1 che obj2 vengono catturati esplicitamente; possiamo vederlo solo guardando il codice. Per la seconda chiusura, possiamo vedere che obj1 viene catturato esplicitamente, ma ReSharper ci sta avvertendo che obj2 viene catturato implicitamente.

Ciò è dovuto a un dettaglio di implementazione nel compilatore C #. Durante la compilazione, le chiusure vengono riscritte in classi con campi che contengono i valori acquisiti e metodi che rappresentano la chiusura stessa. Il compilatore C # creerà solo una di queste classi private per metodo e se in un metodo è definita più di una chiusura, questa classe conterrà più metodi, uno per ogni chiusura e includerà anche tutti i valori acquisiti da tutte le chiusure.

Se osserviamo il codice generato dal compilatore, sembra un po 'così (alcuni nomi sono stati ripuliti per facilitare la lettura):

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

Quando il metodo viene eseguito, crea la classe di visualizzazione, che acquisisce tutti i valori, per tutte le chiusure. Pertanto, anche se un valore non viene utilizzato in una delle chiusure, verrà comunque acquisito. Questa è la cattura "implicita" che ReSharper sta mettendo in evidenza.

L'implicazione di questa ispezione è che il valore di chiusura acquisito in modo implicito non verrà raccolto in modo inutile fino a quando non verrà raccolta la stessa immondizia. La durata di questo valore è ora legata alla durata di una chiusura che non utilizza esplicitamente il valore. Se la chiusura è di lunga durata, ciò potrebbe avere un effetto negativo sul codice, soprattutto se il valore acquisito è molto grande.

Si noti che sebbene si tratti di un dettaglio di implementazione del compilatore, è coerente tra versioni e implementazioni come Microsoft (pre e post Roslyn) o il compilatore di Mono. L'implementazione deve funzionare come descritto per gestire correttamente più chiusure acquisendo un tipo di valore. Ad esempio, se più chiusure acquisiscono un int, devono catturare la stessa istanza, il che può avvenire solo con una singola classe nidificata privata condivisa. L'effetto collaterale di ciò è che la durata di tutti i valori acquisiti è ora la durata massima di qualsiasi chiusura che acquisisce uno qualsiasi dei valori.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.