Variabile acquisita in un ciclo in C #


216

Ho incontrato 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 10 secondi.

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 in modo che ogni istanza di azione abbia la propria variabile acquisita?


15
Vedi anche la serie Blog di Eric Lippert sull'argomento: Chiudere la variabile Loop considerata dannosa
Brian

10
Inoltre, stanno cambiando C # 5 per funzionare come previsto in una foreach. (cambio di rottura)
Neal Tibrewala


3
@Neal: anche se questo esempio non funziona ancora correttamente in C # 5, dato che genera ancora cinque 10s
Ian Oakes

6
Ha verificato che ha prodotto cinque 10s fino ad oggi su C # 6.0 (VS 2015). Dubito che questo comportamento delle variabili di chiusura sia un candidato al cambiamento. Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured.
RBT

Risposte:


197

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

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

Puoi pensarlo come se il compilatore C # crea una "nuova" 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 utilizzando foro 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 e anche il mio articolo sulle chiusure contiene più esempi.

Si noti che a partire dal compilatore C # 5 e oltre (anche quando si specifica una versione precedente di C #), il comportamento di è foreachcambiato, quindi non è più necessario eseguire una copia locale. Vedi questa risposta per maggiori dettagli.


32
Il libro di Jon ha anche un ottimo capitolo su questo (smetti di essere umile, Jon!)
Marc Gravell

35
Sembra migliore se permetto ad altre persone di collegarlo;) (Confesso che tendo a votare le risposte raccomandandolo però.)
Jon Skeet,

2
Come sempre, il feedback a skeet@pobox.com sarebbe apprezzato :)
Jon Skeet,

7
Per C # 5.0 comportamento è diverso (più ragionevole) vedere la risposta più recente di Jon Skeet - stackoverflow.com/questions/16264289/...
Alexei Levenkov

1
@Florimond: Non è così che funzionano le chiusure in C #. Catturano variabili , non valori . (Questo è vero indipendentemente dai loop, ed è facilmente dimostrato con un lambda che cattura una variabile e stampa il valore corrente ogni volta che viene eseguito.)
Jon Skeet

23

Credo che ciò che stai vivendo sia qualcosa noto come Chiusura http://it.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.


11

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.


8

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

IE

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

Vedi la spiegazione nella mia risposta modificata. Sto trovando il pezzo rilevante delle specifiche ora.
Jon Skeet,

Haha jon, in realtà ho appena letto il tuo articolo: csharpindepth.com/Articles/Chapter5/Closures.aspx Fai un buon lavoro amico mio.
tjlevine,

@tjlevine: Mille Grazie. Aggiungerò un riferimento a quello nella mia risposta. Me ne ero dimenticato!
Jon Skeet,

Inoltre, Jon, mi piacerebbe leggere i tuoi pensieri sulle varie proposte di chiusura di Java 7. Ti ho visto menzionare che volevi scriverne uno, ma non l'ho visto.
tjlevine,

1
@tjlevine: Okay, prometto di provare a scriverlo entro la fine dell'anno :)
Jon Skeet,

6

Questo non ha nulla a che fare con i loop.

Questo comportamento viene attivato perché si utilizza un'espressione lambda in () => variable * 2cui l'ambito esterno variablenon è effettivamente definito nell'ambito interno dell'agnda.

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 fa C # per risolvere tutti questi dilemmi è creare una nuova classe helper ("chiusura") con campi corrispondenti alle variabili locali utilizzate nelle espressioni lambda e metodi corrispondenti ai metodi lambda effettivi. Qualsiasi modifica a variablenel tuo codice viene in realtà tradotta per cambiarlaClosureClass.variable

Quindi il tuo ciclo while continua ad aggiornare il ClosureClass.variablefino a raggiungere 10, quindi tu per i cicli esegui le azioni, che funzionano 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());
}

È inoltre possibile 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, le "chiusure" NON sono un concetto correlato ai loop , ma piuttosto a metodi anonimi / espressioni lambda che utilizzano variabili con ambito locale, sebbene un uso incauto dei loop mostri trappole di chiusure.


5

Sì, è necessario eseguire l'ambito variableall'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();

5

La stessa situazione si sta verificando nel multi-threading (C #, .NET 4.0].

Vedi il seguente codice:

Lo scopo è di 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 sembra aiutarmi. Ancora non deterministico.
Mladen Mihajlovic,

0

Poiché nessuno qui citava direttamente l' ECMA-334 :

10.4.4.10 Per le dichiarazioni

Verifica assegnazione definitiva per una dichiarazione for del modulo:

for (for-initializer; for-condition; for-iterator) embedded-statement

viene fatto come se la dichiarazione fosse scritta:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

Più avanti nelle specifiche,

12.16.6.3 Istantanea di variabili locali

Una variabile locale viene considerata come un'istanza quando l'esecuzione entra nell'ambito della variabile.

[Esempio: ad esempio, quando viene invocato il seguente metodo, la variabile locale xviene istanziata e inizializzata tre volte, una volta per ogni iterazione del ciclo.

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

Tuttavia, spostando la dichiarazione di xfuori del ciclo si ottiene una singola istanza di x:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

fine esempio]

Quando non viene acquisito, non è possibile osservare con esattezza la frequenza con cui viene istanziata una variabile locale, poiché la durata delle istanze è disgiunta, per ciascuna istanza è possibile utilizzare semplicemente la stessa posizione di archiviazione. Tuttavia, quando una funzione anonima acquisisce una variabile locale, gli effetti dell'istanza diventano evidenti.

[Esempio: l'esempio

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

produce l'output:

1
3
5

Tuttavia, quando la dichiarazione di xviene spostata all'esterno del ciclo:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

l'output è:

5
5
5

Si noti che al compilatore è consentito (ma non necessario) di ottimizzare le tre istanze in una singola istanza delegato (§11.7.2).

Se un ciclo for dichiara una variabile di iterazione, quella stessa variabile viene considerata dichiarata al di fuori del ciclo. [Esempio: Pertanto, se l'esempio viene modificato per acquisire la variabile di iterazione stessa:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

viene catturata solo un'istanza della variabile di iterazione, che produce l'output:

3
3
3

fine esempio]

Oh sì, immagino che si dovrebbe menzionare che in C ++ questo problema non si verifica perché puoi scegliere se la variabile viene catturata per valore o per riferimento (vedi: Lambda capture ).


-1

Si chiama problema di chiusura, basta usare una variabile di copia ed è fatto.

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());
}

4
In che modo la tua risposta è diversa dalla risposta fornita da qualcuno sopra?
Thangadurai,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.