2010-07-01 15 views
5

Ho uno snippet di codice che pensavo avrebbe funzionato a causa delle chiusure; tuttavia, il risultato dimostra il contrario. Cosa sta succedendo qui perché non produca l'output atteso (una parola per parola)?Comportamento dispari (loop/thread/stringa/lambda) in C#

Codice:

string[] source = new string[] {"this", "that", "other"}; 
List<Thread> testThreads = new List<Thread>(); 
foreach (string text in source) 
{ 
    testThreads.Add(new Thread(() => 
    { 
     Console.WriteLine(text); 
    })); 
} 

testThreads.ForEach(t => t.Start()) 

uscita:

other 
other 
other 
+7

eccoci di nuovo .... – leppie

+1

Questo dovrebbe essere un avvertimento del compilatore come in VB. Penso che sia stupido per il team C# non avvertire le persone dei potenziali problemi. –

+0

Duplicato di: [C# - L'identificativo e le chiusure foreach] (http://stackoverflow.com/questions/512166/c-the-foreach-identifier-and-closures) – Shog9

risposta

7

Questo ha a che fare con il fatto che le chiusure catturare la variabile stessa senza valutare fino a quando è effettivamente utilizzato. Dopo la fine del ciclo foreach il valore di text è "altro", e come dopo il ciclo termina che il metodo viene richiamato e al momento dell'invocazione il valore della variabile acquisita text è "altro" See this blog post from Eric Lippert per i dettagli. Spiega il comportamento e alcune delle ragioni alla base.

+0

In particolare, una chiusura viene eseguita solo quando invocata, non quando dichiarato. Nel momento in cui viene invocato, l'affermazione di Davy8 è ciò che accade. – spoulson

+0

Modificato per, si spera, essere più chiaro. – Davy8

2

Le chiusure in C# non acquisiscono il valore del testo al momento della creazione. Poiché il ciclo foreach termina l'esecuzione prima dell'esecuzione di qualsiasi thread, viene assegnato a ciascuno l'ultimo valore di text.

Questo si può rimediare:

string[] source = new string[] {"this", "that", "other"}; 
List<Thread> testThreads = new List<Thread>(); 

foreach (string text in source) 
{ 
    // Capture the text before using it in a closure 
    string capturedText = text; 

    testThreads.Add(new Thread(() => 
     { 
      Console.WriteLine(capturedText); 
     })); 
} 

testThreads.ForEach(t => t.Start()); 

Come si può vedere, il codice "cattura" il valore di text all'interno di ogni iterazione del ciclo for. Questo garantisce che la chiusura abbia un riferimento univoco per ogni iterazione piuttosto che condividere lo stesso riferimento alla fine.

0

La ragione per cui questo sta accadendo è che nel momento in cui si iniziano i thread il ciclo è terminato e il valore della variabile locale del testo è "altro", quindi quando si avvia il thread è ciò che viene stampato. Questo potrebbe essere facilmente risolto:

string[] source = new string[] {"this", "that", "other"}; 
foreach (string text in source) 
{ 
    new Thread(t => Console.WriteLine(t)).Start(text); 
} 
0

Altri hanno spiegato il motivo per cui si sta incontrando questo problema.

Fortunatamente, la correzione è molto facile:

foreach (string text in source) 
{ 
    string textLocal = text; // this is all you need to add 
    testThreads.Add(new Thread(() => 
    { 
     Console.WriteLine(textLocal); // well, and change this line 
    })); 
} 
4

Questo è un classico errore di catturare una variabile del ciclo. Ciò riguarda sia i cicli for e foreach: presupponendo una costruzione tipica, si dispone di una singola variabile per l'intera durata del ciclo. Quando una variabile viene catturata da un'espressione lambda o da un metodo anonimo, è la variabile stessa (non il valore al momento dell'acquisizione) che viene catturata. Se si modifica il valore della variabile e quindi si esegue il delegato, il delegato "vedrà" quella modifica.

Eric Lippert è molto dettagliato nel suo blog: part 1, part 2.

La soluzione più comune è quello di prendere una copia della variabile all'interno del ciclo:

string[] source = new string[] {"this", "that", "other"}; 
List<Thread> testThreads = new List<Thread>(); 
foreach (string text in source) 
{ 
    string copy = text; 
    testThreads.Add(new Thread(() => 
    { 
     Console.WriteLine(copy); 
    })); 
} 

testThreads.ForEach(t => t.Start()) 

Il motivo per cui funziona è che ogni delegato ora catturare una "istanza" diverso della variabile copy.La variabile acquisita sarà quella creata per l'iterazione del ciclo, a cui viene assegnato il valore di textper tale iterazione. Ecco, tutto funziona.

0

Le chiusure/lambda non si possono associare correttamente alle variabili contatore foreach o loop. Copia il valore in un'altra variabile locale (non dichiarata come variabile foreach o contatore) e funzionerà come previsto.

Problemi correlati