2015-11-24 19 views
15

mi stava indagando alcuni strani problemi oggetto di corso della vita, e sono imbattuto in questo comportamento molto imbarazzante del compilatore C#:Questo comportamento di chiusura è un errore del compilatore C#?

Si consideri il seguente classe di test:

class Test 
{ 
    delegate Stream CreateStream(); 

    CreateStream TestMethod(IEnumerable<string> data) 
    { 
     string file = "dummy.txt"; 
     var hashSet = new HashSet<string>(); 

     var count = data.Count(s => hashSet.Add(s)); 

     CreateStream createStream =() => File.OpenRead(file); 

     return createStream; 
    } 
} 

il compilatore genera il seguente:

internal class Test 
{ 
    public Test() 
    { 
    base..ctor(); 
    } 

    private Test.CreateStream TestMethod(IEnumerable<string> data) 
    { 
    Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0(); 
    cDisplayClass10.file = "dummy.txt"; 
    cDisplayClass10.hashSet = new HashSet<string>(); 
    Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0))); 
    return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1)); 
    } 

    private delegate Stream CreateStream(); 

    [CompilerGenerated] 
    private sealed class <>c__DisplayClass1_0 
    { 
    public HashSet<string> hashSet; 
    public string file; 

    public <>c__DisplayClass1_0() 
    { 
     base..ctor(); 
    } 

    internal bool <TestMethod>b__0(string s) 
    { 
     return this.hashSet.Add(s); 
    } 

    internal Stream <TestMethod>b__1() 
    { 
     return (Stream) File.OpenRead(this.file); 
    } 
    } 
} 

La classe originale contiene due lambda: s => hashSet.Add(s) e () => File.OpenRead(file). Il primo si chiude sulla variabile locale hashSet, la seconda si chiude sulla variabile locale file. Tuttavia, il compilatore genera una classe di implementazione a chiusura singola <>c__DisplayClass1_0 che contiene sia hashSet sia file. Di conseguenza, il delegato restituito CreateStream contiene e mantiene in vita un riferimento all'oggetto hashSet che avrebbe dovuto essere disponibile per GC una volta restituito TestMethod.

Nello scenario effettivo in cui ho riscontrato questo problema, un oggetto molto consistente (ad esempio,> 100mb) è racchiuso in modo errato.

Le mie domande specifiche sono:

  1. Si tratta di un bug? In caso contrario, perché questo comportamento è considerato desiderabile?

Aggiornamento:

Il C# 5 spec 7.15.5.1 dice:

Quando una variabile esterna fa riferimento a una funzione anonima, la variabile esterna si dice che sono stati catturati dalla funzione anonima . In genere, la durata di una variabile locale è limitata all'effettuazione del blocco o dell'istruzione (§5.1.7). . Tuttavia, la durata di una variabile esterna catturata è estesa almeno fino a quando il delegato o la struttura dell'espressione creati da diventano idonei per la garbage collection.

Questo sembra essere aperto ad un certo grado di interpretazione e non proibisce esplicitamente a lambda di catturare variabili a cui non fa riferimento. Tuttavia, this question copre uno scenario correlato, che @ eric-lippert considera un bug. IMHO, vedo l'implementazione di chiusura combinata fornita dal compilatore come una buona ottimizzazione, ma che l'ottimizzazione non dovrebbe essere usata per lambdas che il compilatore può ragionevolmente rilevare potrebbe avere una durata oltre il frame dello stack corrente.


  1. Come faccio a codice contro questo senza abbandonare l'uso di lambda tutti insieme? In particolare, come faccio a codificare questo in modo difensivo, in modo che le future modifiche al codice non causino improvvisamente altri lambda immutati nello stesso metodo per iniziare a racchiudere qualcosa che non dovrebbe?

Aggiornamento:

L'esempio di codice che ho fornito è necessariamente artificiosa. Chiaramente, il refactoring della creazione di lambda su un metodo separato aggira il problema. La mia domanda non è intesa a riguardare le migliori pratiche di progettazione (anche quelle trattate da @ peter-duniho). Piuttosto, dato il contenuto dello TestMethod così com'è, mi piacerebbe sapere se c'è un modo per forzare il compilatore ad escludere il lambda createStream dall'implementazione della chiusura combinata.


Per la cronaca, ho scelto come target .NET 4.6 con VS 2015.

+0

Condivide lo stesso ambito lessicale. forse per quello. –

+1

Possibile duplicato di [metodi anonimi discreti che condividono una classe?] (Http://stackoverflow.com/questions/3885106/discrete-anonymous-methods-sharing-a-class). Come bonus aggiuntivo, questo esempio è piuttosto semplice, ma non * è * inventato. – Brian

+0

È questa la ragione per "chiusura implicitamente bloccata"?Penso di capire che l'avviso è molto meglio ora. Mi sono sempre chiesto perché in alcuni casi un lambda catturasse qualcosa con cui non aveva nulla a che fare. –

risposta

12

Si tratta di un bug?

No. Il compilatore è conforme alle specifiche qui.

Perché questo comportamento è considerato desiderabile?

Non è consigliabile. E 'profondamente infelice, come avete scoperto qui, e come ho descritto nel 2007:

http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx

La C squadra # compilatore ha preso in considerazione la risoluzione di questo in ogni versione dal C# 3.0 e non è mai stata la massima priorità sufficiente. Prendi in considerazione la possibilità di inserire un problema sul sito github di Roslyn (se non ce n'è uno già, potrebbe esserlo).

Personalmente mi piacerebbe vederlo risolto; così com'è è un grande "gotcha".

Come si codifica in questo modo senza abbandonare l'uso di lambda tutti insieme?

La variabile è la cosa che viene catturata. È possibile impostare la variabile hashset su null quando hai finito con esso. Quindi l'unica memoria che viene consumata è la memoria per la variabile, quattro byte, e non la memoria per la cosa a cui si riferisce, che verrà raccolta.

6

Io non sono a conoscenza di nulla nella specifica C# linguaggio che avrebbe dettare esattamente come un compilatore è quello di implementare i metodi anonimi e cattura variabile. Questo è un dettaglio di implementazione.

Ciò che la specifica fa è impostare alcune regole per il comportamento dei metodi anonimi e delle loro variabili di acquisizione. Non ho una copia delle specifiche C# 6, ma qui è il testo rilevante dalle specifiche C# 5, in "7.15.5.1 variabili esterne catturata":

& hellip; la durata di una variabile esterna catturata è esteso almeno fino al l'albero di delegati o espressioni creato dalla funzione anonima diventa idoneo per la garbage collection. [sottolineatura mia]

Non c'è nulla nella specifica che limita la durata della variabile. Il compilatore è semplicemente necessario per assicurarsi che la variabile viva abbastanza a lungo da rimanere valida se necessario con il metodo anonimo.

So & hellip;

1.È un errore? In caso contrario, perché questo comportamento è considerato desiderabile?

Non è un bug. Il compilatore è conforme alle specifiche.

Per quanto riguarda se è considerato "desiderabile", è un termine caricato. Ciò che è "desiderabile" dipende dalle tue priorità. Detto questo, una priorità dell'autore del compilatore è semplificare il compito del compilatore (e così facendo, farlo funzionare più velocemente e ridurre le possibilità di bug). Questa particolare implementazione potrebbe essere considerata "desiderabile" in quel contesto.

D'altra parte, i progettisti di linguaggi e gli autori di compilatori hanno anche un obiettivo condiviso di aiutare i programmatori a produrre codice funzionante. Nella misura in cui un dettaglio di implementazione può interferire con questo, un simile dettaglio di implementazione potrebbe essere considerato "indesiderabile". In definitiva, si tratta di classificare ciascuna di queste priorità, in base ai loro obiettivi potenzialmente concorrenti.

2.Come codice contro questo senza abbandonare l'uso di lambda tutti insieme? In particolare, come faccio a codificare questo in modo difensivo, in modo che le future modifiche al codice non causino improvvisamente altri lambda immutati nello stesso metodo per iniziare a racchiudere qualcosa che non dovrebbe?

Difficile dire senza un esempio meno forzato. In generale, direi che la risposta ovvia è "non mescolare i tuoi lambda in quel modo". Nel tuo particolare esempio (certamente inventato), hai un metodo che sembra fare due cose completamente diverse. Questo è generalmente disapprovato per una serie di motivi, e mi sembra che questo esempio si aggiunga a quella lista.

Non so quale sia il modo migliore per sistemare le "due cose diverse", ma un'alternativa ovvia sarebbe il refactoring almeno del metodo in modo che il metodo "due cose diverse" deleghi il lavoro a altri due metodi, ciascuno con un nome descrittivo (che ha il vantaggio di aiutare il codice a essere auto-documentante).

Ad esempio:

CreateStream TestMethod(IEnumerable<string> data) 
{ 
    string file = "dummy.txt"; 
    var hashSet = new HashSet<string>(); 

    var count = AddAndCountNewItems(data, hashSet); 

    CreateStream createStream = GetCreateStreamCallback(file); 

    return createStream; 
} 

int AddAndCountNewItems(IEnumerable<string> data, HashSet<string> hashSet) 
{ 
    return data.Count(s => hashSet.Add(s)); 
} 

CreateStream GetCreateStreamCallback(string file) 
{ 
    return() => File.OpenRead(file); 
} 

In questo modo, le variabili acquisite rimangono indipendenti. Anche se il compilatore, per qualche strana ragione, continua a inserirli entrambi nello stesso tipo di chiusura, non dovrebbe comunque avere la stessa istanza di quel tipo utilizzato tra le due chiusure.

tuo TestMethod() fa ancora due cose diverse, ma almeno sé non contiene quei due implementazioni indipendenti. Il codice è più leggibile e meglio suddiviso in compartimenti, il che è una buona cosa anche oltre al fatto che risolve il problema della durata variabile.

+0

per quanto riguarda la specifica C#, 7.15.5.1, il primo paragrafo inizia _ "Quando una variabile esterna viene referenziata da una funzione anonima, la variabile esterna viene detta catturata dalla funzione anonima" _. Tuttavia, il lambda '() => File.OpenRead (file)' non fa riferimento alla variabile esterna 'hashSet', quindi la durata di' hashSet' non dovrebbe essere estesa dalla durata di questo lambda. Per quanto riguarda le due cose diverse, come si nota, questo è davvero un esempio forzato. Il problema sembra influenzare qualsiasi metodo che utilizza catturare lambda per fare un po 'di lavoro e crea un lambda di cattura di lunga durata. – tg73

+0

@ tg73: _ "quindi la vita di hashSet non dovrebbe essere estesa per tutta la durata di questa lambda" _ - IMHO, non stai leggendo con attenzione le specifiche. La variabile 'hashSet' _è_ catturata dall'espressione _other_ lambda e nulla nella specifica pone un limite _upper_ sulla durata di tali variabili catturate. Se il compilatore lo avesse voluto, potrebbe implementare la cattura rendendo la variabile una variabile 'static' e _never_ scartandola. Pur comprendendo che il comportamento è inopportuno per i tuoi scopi, è completamente conforme ai requisiti stabiliti dalla specifica. –

+0

@ tg73: _ "Il problema sembra influenzare qualsiasi metodo che ... crea un lambda di cattura di lunga durata" _ - ma solo quando si hanno due metodi anonimi non collegati nel metodo in cui ciascuno acquisisce una variabile locale diversa. I metodi dovrebbero essere semplici; un metodo abbastanza grande da contenere due bit di logica indipendenti con vite variabili non correlate è comunque necessario per il refactoring. IMHO dovrebbe essere abbastanza facile da aggirare questo problema in ogni caso, suddividendo il metodo in parti più piccole. Non posso commentare esempi che non ho visto, ma sarebbe inusuale non essere in grado di farlo facilmente. –