Cosa sono le "chiusure" in .NET?


195

Cos'è una chiusura ? Li abbiamo in .NET?

Se esistono in .NET, potresti fornire uno snippet di codice (preferibilmente in C #) che lo spieghi?

Risposte:


258

Ho un articolo proprio su questo argomento . (Ha molti esempi.)

In sostanza, una chiusura è un blocco di codice che può essere eseguito in un secondo momento, ma che mantiene l'ambiente in cui è stato creato per la prima volta, ovvero può ancora utilizzare le variabili locali ecc. Del metodo che l'ha creato, anche dopo il metodo ha terminato l'esecuzione.

La caratteristica generale delle chiusure è implementata in C # con metodi anonimi ed espressioni lambda.

Ecco un esempio usando un metodo anonimo:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement; 
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }
}

Produzione:

counter=1
counter=2

Qui possiamo vedere che l'azione restituita da CreateAction ha ancora accesso alla variabile counter e può effettivamente incrementarla, anche se CreateAction stessa è terminata.


57
Grazie Jon. A proposito, c'è qualcosa che non conosci in .NET? :) A chi vai quando hai domande?
Sviluppatore

44
C'è sempre altro da imparare :) Ho appena finito di leggere CLR via C # - molto istruttivo. A parte questo, di solito chiedo a Marc Gravell gli alberi WCF / binding / expression e Eric Lippert per le cose in linguaggio C #.
Jon Skeet,

2
L'ho notato, ma penso ancora che la tua affermazione sul fatto che sia "un blocco di codice che può essere eseguito in un secondo momento" è semplicemente errata - non ha nulla a che fare con l'esecuzione, più a che fare con i valori delle variabili e l'ambito che con l'esecuzione. , di per sé.
Jason Bunting

11
Direi che le chiusure non sono utili a meno che non possano essere eseguite e che "in un secondo momento" evidenzi la "stranezza" di essere in grado di catturare l'ambiente (che altrimenti potrebbe essere scomparso al momento dell'esecuzione). Se citi solo metà della frase, ovviamente è una risposta incompleta.
Jon Skeet,

4
@SLC: Sì, counterè disponibile per essere incrementato: il compilatore genera una classe che contiene un countercampo e qualsiasi codice a cui fa riferimento counterfinisce per passare attraverso un'istanza di quella classe.
Jon Skeet,

22

Se sei interessato a vedere come C # implementa la chiusura leggi "Conosco il blog della risposta (42)"

Il compilatore genera una classe in background per incapsulare il metodo anoyema e la variabile j

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0} ", this.j);
    }
    public int j;
}

per la funzione:

static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0} ", j);
                     };
    } 
}

Trasformandolo in:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}

Ciao Daniil - La tua risposta è molto utile e volevo andare oltre la tua risposta e dare seguito, ma il collegamento è interrotto. Sfortunatamente, il mio googlefu non è abbastanza buono da trovare dove si è trasferito.
Knox,

10

Le chiusure sono valori funzionali che mantengono valori variabili dal loro ambito originale. C # può usarli sotto forma di delegati anonimi.

Per un esempio molto semplice, prendi questo codice C #:

    delegate int testDel();

    static void Main(string[] args)
    {
        int foo = 4;
        testDel myClosure = delegate()
        {
            return foo;
        };
        int bar = myClosure();

    }

Al termine, la barra verrà impostata su 4 e il delegato myClosure può essere passato in giro per essere utilizzato altrove nel programma.

Le chiusure possono essere utilizzate per molte cose utili, come l'esecuzione ritardata o per semplificare le interfacce: LINQ è principalmente costruito usando le chiusure. Il modo più immediato che risulta utile per la maggior parte degli sviluppatori è l'aggiunta di gestori di eventi ai controlli creati dinamicamente: è possibile utilizzare le chiusure per aggiungere comportamento quando il controllo viene istanziato, anziché archiviare dati altrove.


10
Func<int, int> GetMultiplier(int a)
{
     return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2));  //outputs 4
Console.WriteLine(fn2(3));  //outputs 6
Console.WriteLine(fn3(2));  //outputs 6
Console.WriteLine(fn3(3));  //outputs 9

Una chiusura è una funzione anonima passata al di fuori della funzione in cui viene creata. Mantiene tutte le variabili della funzione in cui è stato creato che utilizza.


4

Ecco un esempio inventato per C # che ho creato da un codice simile in JavaScript:

public delegate T Iterator<T>() where T : class;

public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
        var i = 0; 
        return delegate { return (i < x.Count) ? x[i++] : null; };
}

Quindi, ecco un po 'di codice che mostra come utilizzare il codice sopra ...

var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});

// So, although CreateIterator() has been called and returned, the variable 
// "i" within CreateIterator() will live on because of a closure created 
// within that method, so that every time the anonymous delegate returned 
// from it is called (by calling iterator()) it's value will increment.

string currentString;    
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null

Spero che sia in qualche modo utile.


1
Hai fornito un esempio, ma non hai offerto una definizione generale. Qui dai tuoi commenti desidero che sono "più incentrati sull'ambito", ma sicuramente c'è dell'altro?
ladenedge,

2

Fondamentalmente la chiusura è un blocco di codice che puoi passare come argomento a una funzione. C # supporta chiusure sotto forma di delegati anonimi.

Ecco un semplice esempio:
Il metodo List.Find può accettare ed eseguire un pezzo di codice (chiusura) per trovare l'elemento dell'elenco.

// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });

Usando la sintassi di C # 3.0 possiamo scrivere questo come:

ints.Find(value => value == 1);

1
Odio essere tecnico, ma la chiusura ha più a che fare con l'ambito: una chiusura può essere creata in un paio di modi diversi, ma una chiusura non è il mezzo, è il fine.
Jason Bunting

2

Una chiusura è quando una funzione è definita all'interno di un'altra funzione (o metodo) e utilizza le variabili dal metodo parent . Questo uso di variabili che si trovano in un metodo e racchiuso in una funzione definita al suo interno, è chiamato chiusura.

Mark Seemann ha alcuni interessanti esempi di chiusure nel suo post sul blog in cui fa un parallelo tra oop e programmazione funzionale.

E per renderlo più dettagliato

var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
    {
        var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
        return File.ReadAllText(path);
    };//the entire process is called a closure.

1

Le chiusure sono blocchi di codice che fanno riferimento a una variabile al di fuori di loro, (da sotto di essi nello stack), che potrebbe essere chiamata o eseguita in un secondo momento (come quando viene definito un evento o un delegato e potrebbe essere chiamata in un determinato momento futuro indefinito ) ... Poiché la variabile esterna a cui il blocco di riferimenti di codice potrebbe non rientrare nell'ambito di applicazione (e che altrimenti sarebbe stato perso), il fatto che sia referenziato dal blocco di codice (chiamato chiusura) dice al runtime di "trattenere "quella variabile nell'ambito fino a quando non è più necessaria dal blocco di chiusura del codice ...


Come ho indicato sulla spiegazione di qualcun altro: odio essere tecnico, ma la chiusura ha più a che fare con l'ambito: una chiusura può essere creata in un paio di modi diversi, ma una chiusura non è il mezzo, è il fine.
Jason Bunting

1
Le chiusure sono relativamente nuove per me, quindi è del tutto possibile fraintendere, ma ottengo la parte dell'ambito. La mia risposta è focalizzata sull'ambito. Quindi mi manca ciò che il tuo commento sta cercando di correggere ... Cos'altro può essere pertinente se non un pezzo di codice? (funzione, metodo anonimo o altro)
Charles Bretana,

Non è la chiave di una chiusura che un "pezzo di codice eseguibile" può accedere a una variabile o a un valore in memoria che sintatticamente "al di fuori" del suo ambito, dopo che quella variabile avrebbe dovuto normalmente andare "fuori campo" o essere stata distrutta ?
Charles Bretana,

E @Jason, non preoccuparti di essere tecnico, questa idea di chiusura è qualcosa che mi ci è voluto un po 'per avvolgermi la testa, in lunghe discussioni con un collega, sulle chiusure javascript ... ma era un pazzo di Lisp e non ho mai abbastanza superato le astrazioni nelle sue spiegazioni ...
Charles Bretana

0

Ho cercato anche di capirlo, ben sotto ci sono gli snippet di codice per lo stesso codice in Javascript e C # che mostrano la chiusura.

  1. Numero di volte in cui si è verificato ogni evento o numero di volte in cui si fa clic su ciascun pulsante.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
      d++;
      alert(d);
  }

  return inner;
};

var a = c();
var b = c();

<body>
<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="b()"/>
</body>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    int b = 0;

    public  void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
  1. conteggio del numero totale di volte in cui si è verificato un evento clic o conteggio del numero totale di clic indipendentemente dal controllo.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
     d++;
     alert(d);
  }

  return inner;
};

var a = c();

<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="a()"/>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    static int b = 0;

    public void call()
    {
      b++;
     Console.WriteLine(b);
    }
}

0

Appena uscito dal nulla, una risposta semplice e comprensiva dal libro C # 7.0 in breve.

Prerequisiti che dovresti conoscere : un'espressione lambda può fare riferimento alle variabili e ai parametri locali del metodo in cui è definita (variabili esterne).

    static void Main()
    {
    int factor = 2;
   //Here factor is the variable that takes part in lambda expression.
    Func<int, int> multiplier = n => n * factor;
    Console.WriteLine (multiplier (3)); // 6
    }

Parte reale : le variabili esterne a cui fa riferimento un'espressione lambda sono chiamate variabili acquisite. Un'espressione lambda che cattura le variabili è chiamata chiusura.

Ultimo punto da notare : le variabili acquisite vengono valutate quando il delegato viene effettivamente richiamato, non quando le variabili sono state acquisite:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30

0

Se si scrive un metodo anonimo in linea (C # 2) o (preferibilmente) un'espressione Lambda (C # 3 +), viene ancora creato un metodo effettivo. Se quel codice utilizza una variabile locale di ambito esterno, è comunque necessario in qualche modo passare quella variabile al metodo.

es. prendi questa clausola Linq Where (che è un semplice metodo di estensione che passa un'espressione lambda):

var i = 0;
var items = new List<string>
{
    "Hello","World"
};   
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time 
{
    i++;
    return true;
});

se vuoi usare i in quell'espressione lambda, devi passarlo a quel metodo creato.

Quindi la prima domanda che si pone è: dovrebbe essere passato per valore o riferimento?

Il passaggio per riferimento è (immagino) più preferibile quando ottieni l'accesso in lettura / scrittura a quella variabile (e questo è ciò che fa C #; immagino che il team di Microsoft abbia valutato i pro e i contro e abbia seguito il riferimento; Secondo Jon Skeet articolo , Java è andato con per valore).

Ma poi sorge un'altra domanda: dove assegnare che io?

Dovrebbe essere effettivamente / naturalmente allocato in pila? Bene, se lo allocate nello stack e lo passate per riferimento, ci possono essere situazioni in cui sopravvive al proprio frame dello stack. Prendi questo esempio:

static void Main(string[] args)
{
    Outlive();
    var list = whereItems.ToList();
    Console.ReadLine();
}

static IEnumerable<string> whereItems;

static void Outlive()
{
    var i = 0;
    var items = new List<string>
    {
        "Hello","World"
    };            
    whereItems = items.Where(x =>
    {
        i++;
        Console.WriteLine(i);
        return true;
    });            
}

L'espressione lambda (nella clausola Where) crea nuovamente un metodo che fa riferimento a una i. Se i è allocato sullo stack di Outlive, quando si enumerano whereItems, l'i utilizzato nel metodo generato punterà all'i di Outlive, ovvero a un punto dello stack che non è più accessibile.

Ok, allora ne abbiamo bisogno sul mucchio.

Quindi, ciò che il compilatore C # fa per supportare questo inline anonymous / lambda, è usare ciò che viene chiamato " Closures ": crea una classe sull'heap chiamata ( piuttosto male ) DisplayClass che ha un campo contenente l'i e la funzione che effettivamente utilizza esso.

Qualcosa che sarebbe equivalente a questo (puoi vedere l'IL generato usando ILSpy o ILDASM):

class <>c_DisplayClass1
{
    public int i;

    public bool <GetFunc>b__0()
    {
        this.i++;
        Console.WriteLine(i);
        return true;
    }
}

Crea un'istanza di quella classe nell'ambito locale e sostituisce qualsiasi codice relativo a i o all'espressione lambda con quell'istanza di chiusura. Quindi, ogni volta che si utilizza l'i nel codice "ambito locale" in cui sono stato definito, si sta effettivamente utilizzando quel campo di istanza DisplayClass.

Quindi, se cambierei l'i "locale" nel metodo principale, cambierebbe effettivamente _DisplayClass.i;

vale a dire

var i = 0;
var items = new List<string>
{
    "Hello","World"
};  
var filtered = items.Where(x =>
{
    i++;
    return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10;            // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12

verrà stampato 12, poiché "i = 10" va a quel campo di dispalyclass e lo cambia appena prima della seconda enumerazione.

Una buona fonte sull'argomento è questo modulo di Pluralsight di Bart De Smet (richiede la registrazione) (ignora anche il suo uso errato del termine "Hoisting" - ciò che (penso) intende che è che la variabile locale (cioè i) è cambiata per fare riferimento nel nuovo campo DisplayClass).


In altre notizie, sembra esserci un malinteso secondo cui le "chiusure" sono correlate ai loop - come ho capito, le "chiusure" NON sono un concetto correlato ai loop , ma piuttosto a metodi anonimi / espressioni lambda uso di variabili con ambito locale - sebbene qualche trucco le domande usano i loop per dimostrarlo.


-1

Una chiusura è una funzione, definita all'interno di una funzione, che può accedere alle variabili locali di essa e al suo genitore.

public string GetByName(string name)
{
   List<things> theThings = new List<things>();
  return  theThings.Find<things>(t => t.Name == name)[0];
}

quindi la funzione all'interno del metodo find.

 t => t.Name == name

può accedere alle variabili all'interno del suo ambito, t, e il nome della variabile che si trova nel suo ambito padre. Anche se viene eseguito dal metodo find come delegato, da un altro ambito tutti insieme.


2
Una chiusura non è una funzione, di per sé, è più definita parlando di ambito che di funzioni. Le funzioni aiutano semplicemente a mantenere l'ambito circostante, causando la creazione di una chiusura. Ma dire che una chiusura è una funzione non è tecnicamente corretta. Mi dispiace nitpick. :)
Jason Bunting
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.