Intero gestito come tipo di riferimento quando passato a un delegato

Ho partecipato ai TechDays 2013 nei Paesi Bassi questa settimana e ho ricevuto un’interrogazione interessante. La domanda era: qual è l’output del seguente programma. Ecco come appare il codice.

class Program { delegate void Writer(); static void Main(string[] args) { var writers = new List(); for (int i = 0; i < 10; i++) { writers.Add(delegate { Console.WriteLine(i); }); } foreach (Writer writer in writers) { writer(); } } } 

Ovviamente, la risposta che ho dato era sbagliata. I argumentend, perché int è un tipo di valore, il valore effettivo che viene passato in Console.WriteLine() viene copiato, quindi l’output sarà 0 … 9. Comunque i sono gestito come un tipo di riferimento in questa situazione. La risposta corretta è che visualizzerà dieci volte 10. Qualcuno può spiegare perché e come?

I argumentend, perché int è un tipo di valore, il valore effettivo che viene passato in Console.WriteLine () viene copiato

Questo è esattamente corretto. Quando si chiama WriteLine il valore verrà copiato .

Quindi, quando chiami WriteLine ? Non è nel ciclo for . Non stai scrivendo nulla in quel momento, stai solo creando un delegato.

Non è fino al ciclo foreach quando si richiama il delegato, è in quel momento che il valore nella variabile i viene copiato nello stack per la chiamata a WriteLine .

Quindi, qual è il valore di i durante il ciclo foreach ? È 10, per ogni iterazione del ciclo foreach .

Quindi ora stai chiedendo, “beh, come sto tutto durante il foreach loop, isn't it out of scope ?” Beh, no, non lo è. “Ciò che sta dimostrando è una” chiusura “.Quando un metodo anonimo fa riferimento a una variabile l’ambito di questa variabile deve durare fino a quel metodo anonimo, che potrebbe essere per qualsiasi periodo di tempo. Se non viene fatto nulla di speciale, la variabile sarà spazzatura casuale contenente tutto ciò che è accaduto in quella posizione in memoria. si assicura triggersmente che la situazione non possa accadere.

Quindi, cosa fa? Crea una class di chiusura; è una class che conterrà un numero di campi che rappresentano tutto ciò che è chiuso. In altre parole, il codice verrà rifattorizzato per apparire in questo modo:

 public class ClosureClass { public int i; public void DoStuff() { Console.WriteLine(i); } } class Program { delegate void Writer(); static void Main(string[] args) { var writers = new List(); ClosureClass closure = new ClosureClass(); for (closure.i = 0; closure.i < 10; closure.i++) { writers.Add(closure.DoStuff); } foreach (Writer writer in writers) { writer(); } } } 

Ora entrambi abbiamo un nome per il nostro metodo anonimo (tutti i metodi anonimi hanno un nome dal compilatore) e possiamo garantire che la variabile rimanga valida fino a quando il delegato che fa riferimento alla funzione anonima vive.

Guardando questo refactore, spero sia chiaro perché il risultato è che 10 è stampato 10 volte.

È perché è una variabile catturata. Nota che questo accadeva anche con foreach , ma che è cambiato in C # 5. Ma per riscrivere il tuo codice su ciò che effettivamente hai:

 class Program { delegate void Writer(); class CaptureContext { // generated by the compiler and named something public int i; // truly horrible that is illegal in C# public void DoStuff() { Console.WriteLine(i); } } static void Main(string[] args) { var writers = new List(); var ctx = new CaptureContext(); for (ctx.i = 0; ctx.i < 10; ctx.i++) { writers.Add(ctx.DoStuff); } foreach (Writer writer in writers) { writer(); } } } 

Come potete vedere: c'è solo un ctx quindi solo un ctx.i , ed è 10 per il momento in cui si writers su writers .

A proposito, se vuoi far funzionare il vecchio codice:

 for (int tmp = 0; tmp < 10; tmp++) { int i = tmp; writers.Add(delegate { Console.WriteLine(i); }); } 

Fondamentalmente, il contesto di cattura ha un ambito allo stesso livello della variabile; qui la variabile è circoscritta all'interno del ciclo, quindi questo genera:

 for (int tmp = 0; tmp < 10; tmp++) { var ctx = new CaptureContext(); ctx.i = tmp; writers.Add(ctx.DoStuff); } 

Qui ogni DoStuff trova su un'istanza di contesto di acquisizione diversa, quindi ha un i diverso e separato.

Nel tuo caso, i metodi delegati sono metodi anonimi che accedono a una variabile locale ( per l’ indice loop i ). Cioè, questi sono clousures .

Poiché il metodo anonimo viene chiamato dieci volte dopo il ciclo for , ottiene il valore più recente per i .

Un semplice esempio di varie cusherure che accedono allo stesso riferimento

Ecco una versione semplificata del comportamento di clousure:

 int a = 1; Action a1 = () => Console.WriteLine(a); Action a2 = () => Console.WriteLine(a); Action a3 = () => Console.WriteLine(a); a = 2; // This will print 3 times the latest assigned value of `a` (2) variable instead // of just 1. a1(); a2(); a3(); 

Controlla questo altro Q & A ( Quali sono i clousures in .NET? ) Su StackOverflow per maggiori informazioni su quali sono Cusuure C # /. NET!

Per me, è più facile da capire confrontando il vecchio comportamento e il nuovo comportamento con la class Action nativa al posto di un Writer personalizzato.

Prima della chiusura del C # 5 veniva catturata la stessa variabile (non il valore della variabile) in casi di for, variabili foreach e acquisizioni di variabili locali. Quindi dato il codice:

  var anonymousFunctions = new List(); var listOfNumbers = Enumerable.Range(0, 10); for (int forLoopVariable = 0; forLoopVariable < 10; forLoopVariable++) { anonymousFunctions.Add(delegate { Console.WriteLine(forLoopVariable); });//outputs 10 every time. } foreach (Action writer in anonymousFunctions) { writer(); } 

Vediamo solo l'ultimo valore che abbiamo impostato per la variabile forLoopVariable . Tuttavia, con C # 5, il ciclo foreach è stato modificato. Ora acquisiamo variabili distinte.

PER ESEMPIO

  anonymousFunctions.Clear();//C# 5 foreach loop captures foreach (var i in listOfNumbers) { anonymousFunctions.Add(delegate { Console.WriteLine(i); });//outputs entire range of numbers } foreach (Action writer in anonymousFunctions) { writer(); } 

Quindi l'output è più intuitivo: 0,1,2 ...

Si noti che questo è un cambio di rottura (anche se si presume che sia un minore). E questo potrebbe essere il motivo per cui il comportamento del ciclo for rimane invariato con C # 5.