Domanda

Ho riscontrato un problema interessante su C #. Ho un codice come sotto.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Mi aspetto che produca 0, 2, 4, 6, 8. Tuttavia, in realtà produce cinque 10s.

Sembra che sia dovuto a tutte le azioni che si riferiscono a una variabile acquisita. Di conseguenza, quando vengono invocati, hanno tutti lo stesso output.

Esiste un modo per aggirare questo limite affinché ogni istanza di azione abbia la propria variabile acquisita?

È stato utile?

Soluzione

Sì - prendi una copia della variabile all'interno del ciclo:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

Puoi pensarlo come se il compilatore C # crea un " nuovo " variabile locale ogni volta che colpisce la dichiarazione della variabile. In effetti creerà nuovi oggetti di chiusura appropriati e diventerà complicato (in termini di implementazione) se si fa riferimento a variabili in più ambiti, ma funziona :)

Si noti che un'occorrenza più comune di questo problema sta usando per o foreach :

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

Vedi la sezione 7.14.4.2 della specifica C # 3.0 per maggiori dettagli su questo e il mio su chiusure contiene anche altri esempi.

Altri suggerimenti

Credo che ciò che stai vivendo sia qualcosa di noto come Chiusura http: //en.wikipedia. org / wiki / Closure_ (computer_science) . La tua lamba ha un riferimento a una variabile che ha un ambito esterno alla funzione stessa. La tua lamba non viene interpretata finché non la invochi e una volta ottenuta otterrà il valore della variabile al momento dell'esecuzione.

Dietro le quinte, il compilatore sta generando una classe che rappresenta la chiusura per la chiamata del metodo. Utilizza quella singola istanza della classe di chiusura per ogni iterazione del ciclo. Il codice è simile a questo, il che rende più facile capire perché si verifica il bug:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

Questo non è in realtà il codice compilato dal tuo esempio, ma ho esaminato il mio codice e questo assomiglia molto a ciò che il compilatore avrebbe effettivamente generato.

Il modo per aggirare questo è di memorizzare il valore necessario in una variabile proxy e far catturare quella variabile.

cioè.

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

Sì, è necessario ambito variabile all'interno del ciclo e passarlo alla lambda in questo modo:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

La stessa situazione si sta verificando nel multithreading (C #, .NET 4.0].

Vedi il seguente codice:

Scopo è stampare 1,2,3,4,5 in ordine.

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

L'output è interessante! (Potrebbe essere come 21334 ...)

L'unica soluzione è utilizzare le variabili locali.

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

Questo non ha nulla a che fare con i loop.

Questo comportamento è attivato perché usi un'espressione lambda () = > variabile * 2 dove la variabile con ambito esterno non è in realtà definita nell'ambito interno del lambda.

Le espressioni Lambda (in C # 3 + e i metodi anonimi in C # 2) creano ancora metodi effettivi. Passare le variabili a questi metodi implica alcuni dilemmi (passa per valore? Passa per riferimento? C # va per riferimento - ma questo apre un altro problema in cui il riferimento può sopravvivere alla variabile effettiva). Ciò che C # fa per risolvere tutti questi dilemmi è creare una nuova classe di supporto ("chiusura") con campi corrispondenti alle variabili locali utilizzate nelle espressioni lambda e metodi corrispondenti ai metodi lambda effettivi. Qualsiasi modifica a variabile nel tuo codice viene effettivamente tradotta per cambiare in quel ClosureClass.variable

Quindi il tuo ciclo while continua ad aggiornare ClosureClass.variable fino a quando non raggiunge 10, quindi per i loop esegui le azioni, che operano tutte sullo stesso ClosureClass.variable .

Per ottenere il risultato atteso, è necessario creare una separazione tra la variabile loop e la variabile che viene chiusa. Puoi farlo introducendo un'altra variabile, ovvero:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Puoi anche spostare la chiusura in un altro metodo per creare questa separazione:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Puoi implementare Mult come espressione lambda (chiusura implicita)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

o con una classe helper effettiva:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

In ogni caso, " Chiusure " NON sono un concetto correlato ai loop , ma piuttosto a metodi anonimi / espressioni lambda uso di variabili con ambito locale, anche se un uso incauto dei loop dimostra trappole di chiusure.

Si chiama problema di chiusura, usa semplicemente una variabile di copia ed è fatta.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top