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:
- 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.
- 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.
Condivide lo stesso ambito lessicale. forse per quello. –
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
È 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. –