Metodi anonimi discreti che condividono una class?

Stavo giocando un po ‘con la class Ref Eric Lippert da qui . Ho notato nell’IL che sembrava che entrambi i metodi anonimi stessero usando la stessa class generata, anche se ciò significava che la class aveva una variabile extra.

Mentre usare solo una nuova definizione di class sembra abbastanza ragionevole, mi sembra molto strano che venga creata una sola istanza di c__DisplayClass2 . Ciò sembra implicare che entrambe le istanze di Ref stiano facendo riferimento allo stesso c__DisplayClass2 non significa che y non può essere raccolto fino a quando non viene raccolto vart1 , che può accadere molto più tardi rispetto a quando joik ritorna? Dopo tutto, non vi è alcuna garanzia che qualche idiota non scriverà una funzione (direttamente in IL) che accede direttamente a y attraverso vart1 joik restituisce. Forse questo potrebbe anche essere fatto con la riflessione invece che tramite IL pazzo.

 sealed class Ref { public delegate T Func(); private readonly Func getter; public Ref(Func getter) { this.getter = getter; } public T Value { get { return getter(); } } } static Ref joik() { int[] y = new int[50000]; int x = 5; Ref vart1 = new Ref(delegate() { return x; }); Ref vart2 = new Ref(delegate() { return y; }); return vart1; } 

L’esecuzione di IL DASM ha confermato che vart1 e vart2 utilizzavano entrambi __DisplayClass2 , che conteneva un campo pubblico per x e per y. L’IL di joik:

 .method private hidebysig static class Program/Ref`1 joik() cil managed { // Code size 72 (0x48) .maxstack 3 .locals init ([0] class Program/Ref`1 vart1, [1] class Program/Ref`1 vart2, [2] class Program/'c__DisplayClass2' '8__locals3', [3] class Program/Ref`1 CS$1$0000) IL_0000: newobj instance void Program/'c__DisplayClass2'::.ctor() IL_0005: stloc.2 IL_0006: nop IL_0007: ldloc.2 IL_0008: ldc.i4 0xc350 IL_000d: newarr [mscorlib]System.Int32 IL_0012: stfld int32[] Program/'c__DisplayClass2'::y IL_0017: ldloc.2 IL_0018: ldc.i4.5 IL_0019: stfld int32 Program/'c__DisplayClass2'::x IL_001e: ldloc.2 IL_001f: ldftn instance int32 Program/'c__DisplayClass2'::'b__0'() IL_0025: newobj instance void class Program/Ref`1/Func`1::.ctor(object, native int) IL_002a: newobj instance void class Program/Ref`1::.ctor(class Program/Ref`1/Func`1) IL_002f: stloc.0 IL_0030: ldloc.2 IL_0031: ldftn instance int32[] Program/'c__DisplayClass2'::'b__1'() IL_0037: newobj instance void class Program/Ref`1/Func`1::.ctor(object, native int) IL_003c: newobj instance void class Program/Ref`1::.ctor(class Program/Ref`1/Func`1) IL_0041: stloc.1 IL_0042: ldloc.0 IL_0043: stloc.3 IL_0044: br.s IL_0046 IL_0046: ldloc.3 IL_0047: ret } // end of method Program::joik 

    Sì, l’implementazione MS dei metodi anonimi crea effettivamente una class nascosta per livello di ambito di cui ha bisogno per acquisire le variabili e cattura tutte le variabili rilevanti da tale ambito. Credo che ciò sia fatto per semplicità, ma può effettivamente aumentare la vita di alcuni oggetti inutilmente.

    Sarebbe più elegante per ogni metodo anonimo catturare solo le variabili a cui era realmente interessato. Tuttavia, questo potrebbe rendere la vita notevolmente più complicata … se un metodo anonimo cattura y , uno catturato x e uno catturato y , tu Avrei bisogno di tre classi: una per catturare x , una per catturare y , e una per comporre le due (ma non solo avere due variabili). La cosa complicata è che per ogni singola istanza di variabile, quella variabile ha bisogno di vivere esattamente in un punto in modo che tutto ciò che fa riferimento a esso veda lo stesso valore, qualunque esso lo cambi.

    Questo non viola le specifiche in alcun modo, ma potrebbe essere considerato sfortunato – non so se sono effettivamente morsi persone nella vita reale, ma è certamente ansible.

    La buona notizia è che se il team C # decide di migliorare questo, dovrebbero essere in grado di farlo in un modo completamente compatibile all’indietro, a meno che alcuni muppet si affidino a periodi di vita inutilmente estesi.

    Jon ha ovviamente ragione. Il problema che questo di solito causa è:

     void M() { Expensive e = GetExpensive(); Cheap c = GetCheap(); D longLife = ()=>...c...; D shortLife = ()=>...e...; ... } 

    Quindi abbiamo una risorsa costosa la cui durata ora dipende dalla durata di longLife, anche se shortLife viene raccolto in anticipo.

    Questo è sfortunato, ma comune. Le implementazioni delle chiusure in JScript e VB hanno lo stesso problema.

    Mi piacerebbe risolverlo in un’ipotetica versione futura di C # ma non ho garanzie. Il modo ovvio per farlo è identificare le classi di equivalenza delle variabili chiuse in base a quali lambda vengono catturate e generare classi di chiusura una per class di equivalenza, piuttosto che una singola class di chiusura.

    Potrebbero esserci anche cose che potremmo fare con l’analisi di quali variabili closed-over vengono scritte. Come nota Jon, al momento siamo limitati dal nostro bisogno di catturare variabili piuttosto che valori. Potremmo essere più flessibili nella nostra strategia di generazione del codice se identifichiamo le variabili che non vengono mai scritte dopo la creazione della chiusura e le trasformiamo in valori chiusi anziché variabili chiuse.