Cos'è una chiusura?


155

Di tanto in tanto vedo le "chiusure" menzionate, e ho provato a cercarlo, ma Wiki non fornisce una spiegazione che capisco. Qualcuno potrebbe aiutarmi qui?


Se conosci Java / C # spero che questo link sia di aiuto- http://www.developerfusion.com/article/8251/the-beauty-of-closures/
Gulshan,

1
Le chiusure sono difficili da capire. Dovresti provare a fare clic su tutti i collegamenti nella prima frase di quell'articolo di Wikipedia e comprendere prima quegli articoli.
Zach,


3
Qual è la differenza fondamentale tra una chiusura e una classe però? Va bene, una classe con un solo metodo pubblico.
biziclop,

5
@biziclop: potresti emulare una chiusura con una classe (è quello che devono fare gli sviluppatori Java). Ma di solito sono leggermente meno prolissi da creare e non è necessario gestire manualmente ciò che si sta portando in giro. (I lispers hardcore fanno una domanda simile, ma arrivano probabilmente a quell'altra conclusione - che il supporto OO a livello di lingua non è necessario quando si hanno chiusure).

Risposte:


141

(Dichiarazione di non responsabilità: questa è una spiegazione di base; per quanto riguarda la definizione, sto semplificando un po ')

Il modo più semplice di pensare a una chiusura è una funzione che può essere archiviata come variabile (definita "funzione di prima classe"), che ha una capacità speciale di accedere ad altre variabili locali nell'ambito in cui è stata creata.

Esempio (JavaScript):

var setKeyPress = function(callback) {
    document.onkeypress = callback;
};

var initialize = function() {
    var black = false;

    document.onclick = function() {
        black = !black;
        document.body.style.backgroundColor = black ? "#000000" : "transparent";
    }

    var displayValOfBlack = function() {
        alert(black);
    }

    setKeyPress(displayValOfBlack);
};

initialize();

Le funzioni 1 assegnate a document.onclicke displayValOfBlacksono chiusure. Potete vedere che entrambi fanno riferimento alla variabile booleana black, ma quella variabile è assegnata al di fuori della funzione. Poiché blackè locale nell'ambito in cui è stata definita la funzione , il puntatore a questa variabile viene conservato.

Se lo metti in una pagina HTML:

  1. Fai clic per passare al nero
  2. Premi [invio] per vedere "vero"
  3. Fai clic di nuovo, torna al bianco
  4. Premi [invio] per vedere "falso"

Ciò dimostra che entrambi hanno accesso allo stesso black e possono essere utilizzati per memorizzare lo stato senza alcun oggetto wrapper.

La chiamata a setKeyPressè per dimostrare come una funzione può essere passata proprio come qualsiasi variabile. L' ambito conservato nella chiusura è ancora quello in cui è stata definita la funzione.

Le chiusure vengono comunemente utilizzate come gestori di eventi, soprattutto in JavaScript e ActionScript. Un buon uso delle chiusure ti aiuterà a associare implicitamente le variabili ai gestori di eventi senza dover creare un wrapper di oggetti. Tuttavia, un uso non attento porterà a perdite di memoria (come quando un gestore di eventi inutilizzato ma conservato è l'unica cosa da conservare su oggetti di grandi dimensioni in memoria, in particolare oggetti DOM, impedendo la garbage collection).


1: In realtà, tutte le funzioni in JavaScript sono chiusure.


3
Mentre leggevo la tua risposta, ho sentito una lampadina accendermi nella mia mente. Molto apprezzato! :)
Jay,

1
Dato che blackè dichiarato all'interno di una funzione, non verrebbe distrutto quando lo stack si svolgerà ...?
gablin

1
@gablin, questo è ciò che rende uniche le lingue che hanno delle chiusure. Tutte le lingue con Garbage Collection funzionano allo stesso modo: quando non ci sono più riferimenti a un oggetto, può essere distrutto. Ogni volta che una funzione viene creata in JS, l'ambito locale è associato a quella funzione fino a quando tale funzione non viene distrutta.
Nicole,

2
@gablin, questa è una buona domanda. Non penso che non possano & mdash; ma ho solo sollevato la garbage collection da quel che usa JS ed è quello a cui sembravi riferirti quando hai detto "Dato che blackè dichiarato all'interno di una funzione, non verrebbe distrutto". Ricorda anche che se dichiari un oggetto in una funzione e poi lo assegni a una variabile che vive da qualche altra parte, quell'oggetto viene preservato perché ci sono altri riferimenti ad esso.
Nicole,

1
Objective-C (e C sotto clang) supporta blocchi, che sono essenzialmente chiusure, senza garbage collection. Richiede supporto di runtime e alcuni interventi manuali sulla gestione della memoria.
Quixoto,

68

Una chiusura è fondamentalmente solo un modo diverso di guardare un oggetto. Un oggetto è un dato a cui sono associate una o più funzioni. Una chiusura è una funzione a cui sono associate una o più variabili. I due sono sostanzialmente identici, almeno a livello di implementazione. La vera differenza è da dove vengono.

Nella programmazione orientata agli oggetti, si dichiara una classe di oggetti definendo le sue variabili membro e i suoi metodi (funzioni membro) in anticipo, quindi si creano istanze di quella classe. Ogni istanza viene fornita con una copia dei dati dei membri, inizializzata dal costruttore. È quindi possibile disporre di una variabile di un tipo di oggetto e passarla come parte di dati, poiché l'attenzione si concentra sulla sua natura di dati.

In una chiusura, d'altra parte, l'oggetto non viene definito in anticipo come una classe di oggetti o istanziato attraverso una chiamata del costruttore nel codice. Invece, scrivi la chiusura come funzione all'interno di un'altra funzione. La chiusura può fare riferimento a una qualsiasi delle variabili locali della funzione esterna e il compilatore lo rileva e sposta queste variabili dallo spazio di stack della funzione esterna alla dichiarazione di oggetti nascosti della chiusura. Quindi hai una variabile di un tipo di chiusura e anche se è fondamentalmente un oggetto sotto il cofano, lo fai passare come riferimento di funzione, perché l'attenzione è sulla sua natura come funzione.


3
+1: buona risposta. Puoi vedere una chiusura come un oggetto con un solo metodo e un oggetto arbitrario come una raccolta di chiusure su alcuni dati sottostanti comuni (le variabili membro dell'oggetto). Penso che questi due punti di vista siano abbastanza simmetrici.
Giorgio,

3
Ottima risposta In realtà spiega l'intuizione della chiusura.
RoboAlex

1
@Mason Wheeler: dove vengono archiviati i dati di chiusura? In pila come una funzione? O in heap come un oggetto?
RoboAlex

1
@RoboAlex: nell'heap, perché è un oggetto che assomiglia a una funzione.
Mason Wheeler,

1
@RoboAlex: l'archiviazione di una chiusura e dei relativi dati acquisiti dipende dall'implementazione. In C ++ può essere archiviato nell'heap o nello stack.
Giorgio,

29

Il termine chiusura deriva dal fatto che un pezzo di codice (blocco, funzione) può avere variabili libere che sono chiuse (cioè legate a un valore) dall'ambiente in cui è definito il blocco di codice.

Prendiamo ad esempio la definizione della funzione Scala:

def addConstant(v: Int): Int = v + k

Nel corpo della funzione ci sono due nomi (variabili) vche kindicano due valori interi. Il nome vè associato perché viene dichiarato come argomento della funzione addConstant(osservando la dichiarazione della funzione sappiamo che vverrà assegnato un valore quando viene invocata la funzione). Il nome kè libero per la funzione addConstantperché la funzione non contiene alcun indizio su quale valore ksia associato (e come).

Per valutare una chiamata come:

val n = addConstant(10)

dobbiamo assegnare kun valore, che può accadere solo se il nome kè definito nel contesto in cui addConstantè definito. Per esempio:

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  def addConstant(v: Int): Int = v + k

  values.map(addConstant)
}

Ora che abbiamo definito addConstantin un contesto in cui kè definito, addConstantè diventato una chiusura perché tutte le sue variabili libere sono ora chiuse (legate a un valore): addConstantpossono essere invocate e passate in giro come se fossero una funzione. Si noti che la variabile libera kè associata a un valore quando viene definita la chiusura , mentre la variabile argomento vviene associata quando viene invocata la chiusura .

Quindi una chiusura è sostanzialmente una funzione o un blocco di codice che può accedere a valori non locali attraverso le sue variabili libere dopo che queste sono state vincolate dal contesto.

In molte lingue, se si utilizza una chiusura solo una volta, è possibile renderla anonima , ad es

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  values.map(v => v + k)
}

Si noti che una funzione senza variabili libere è un caso speciale di chiusura (con un set vuoto di variabili libere). Analogamente, una funzione anonima è un caso speciale di chiusura anonima , ovvero una funzione anonima è una chiusura anonima senza variabili libere.


Questo si muove bene con le formule chiuse e aperte nella logica. Grazie per la tua risposta.
RainDoctor,

@RainDoctor: le variabili libere sono definite in formule logiche e in espressioni di calcolo lambda in modo simile: la lambda in un'espressione lambda funziona come un quantificatore in formule logiche con variabili libere / associate.
Giorgio,

9

Una semplice spiegazione in JavaScript:

var closure_example = function() {
    var closure = 0;
    // after first iteration the value will not be erased from the memory
    // because it is bound with the returned alertValue function.
    return {
        alertValue : function() {
            closure++;
            alert(closure);
        }
    };
};
closure_example();

alert(closure)utilizzerà il valore precedentemente creato di closure. Lo alertValuespazio dei nomi della funzione restituita sarà collegato allo spazio dei nomi in cui closurerisiede la variabile. Quando si elimina l'intera funzione, il valore della closurevariabile verrà eliminato, ma fino ad allora, la alertValuefunzione sarà sempre in grado di leggere / scrivere il valore della variabile closure.

Se si esegue questo codice, la prima iterazione assegnerà un valore 0 alla closurevariabile e riscriverà la funzione a:

var closure_example = function(){
    alertValue : function(){
        closure++;
        alert(closure);
    }       
}

E poiché alertValuenecessita della variabile locale closureper eseguire la funzione, si lega con il valore della variabile locale precedentemente assegnata closure.

E ora ogni volta che chiami la closure_examplefunzione, scriverà il valore incrementato della closurevariabile perché alert(closure)è associato.

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 

grazie, non ho testato il codice =) ora sembra tutto a posto.
Muha,

5

Una "chiusura" è, in sostanza, uno stato locale e un po 'di codice, combinati in un pacchetto. In genere, lo stato locale proviene da un ambito circostante (lessicale) e il codice è (essenzialmente) una funzione interna che viene quindi restituita all'esterno. La chiusura è quindi una combinazione delle variabili catturate che vede la funzione interna e il codice della funzione interna.

È una di quelle cose che, sfortunatamente, è un po 'difficile da spiegare, a causa della mancanza di familiarità.

Un'analogia che ho usato con successo in passato era "immagina di avere qualcosa che chiamiamo" il libro ", nella chiusura della stanza," il libro "è quella copia lì, nell'angolo, di TAOCP, ma sulla chiusura del tavolo , è quella copia di un libro dei file di Dresda. Quindi, a seconda della chiusura in cui ti trovi, il codice "dammi il libro" provoca diverse cose. "


Hai dimenticato questo: en.wikipedia.org/wiki/Closure_(computer_programming) nella tua risposta.
S. Lott,

3
No, ho consapevolmente scelto di non chiudere quella pagina.
Vatine,

"Stato e funzione": una funzione C con una staticvariabile locale può essere considerata una chiusura? Le chiusure in Haskell coinvolgono lo stato?
Giorgio,

2
@Giorgio Le chiusure di Haskell (credo) chiudono gli argomenti nella portata lessicale in cui sono definiti, quindi direi di sì (anche se nella migliore delle ipotesi non ho familiarità con Haskell). La funzione AC con una variabile statica è, nella migliore delle ipotesi, una chiusura molto limitata (vuoi davvero essere in grado di creare più chiusure da una singola funzione, con una staticvariabile locale, ne hai esattamente una).
Vatine,

Ho posto questa domanda apposta perché penso che una funzione C con una variabile statica non sia una chiusura: la variabile statica è definita localmente e conosciuta solo all'interno della chiusura, non accede all'ambiente. Inoltre, non sono sicuro al 100%, ma formulerei la tua affermazione al contrario: usi il meccanismo di chiusura per creare diverse funzioni (una funzione è una definizione di chiusura + un'associazione per le sue variabili libere).
Giorgio,

5

È difficile definire cosa sia la chiusura senza definire il concetto di "stato".

Fondamentalmente, in un linguaggio con ambito lessicale completo che considera le funzioni come valori di prima classe, succede qualcosa di speciale. Se dovessi fare qualcosa del genere:

function foo(x)
return x
end

x = foo

La variabile xnon solo fa riferimento function foo()ma fa anche riferimento allo stato è foostato lasciato l'ultima volta che è tornato. La vera magia si verifica quando fooaltre funzioni sono ulteriormente definite nel suo ambito; è come il suo mini-ambiente (così come "normalmente" definiamo le funzioni in un ambiente globale).

Funzionalmente può risolvere molti degli stessi problemi della parola chiave 'statica' del C ++ (C?), Che mantiene lo stato di una variabile locale attraverso più chiamate di funzione; tuttavia è più come applicare lo stesso principio (variabile statica) a una funzione, poiché le funzioni sono valori di prima classe; la chiusura aggiunge il supporto per lo stato dell'intera funzione da salvare (nulla a che fare con le funzioni statiche di C ++).

Considerare le funzioni come valori di prima classe e aggiungere il supporto per le chiusure significa anche che è possibile avere più di un'istanza della stessa funzione in memoria (simile alle classi). Ciò significa che è possibile riutilizzare lo stesso codice senza dover reimpostare lo stato della funzione, come richiesto quando si ha a che fare con variabili statiche C ++ all'interno di una funzione (potrebbe esserci di sbagliato in questo?).

Ecco alcuni test del supporto di chiusura di Lua.

--Closure testing
--By Trae Barlow
--

function myclosure()
    print(pvalue)--nil
    local pvalue = pvalue or 10
    return function()
        pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
        print(pvalue)
        pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
        return pvalue
    end
end

x = myclosure() --x now references anonymous function inside myclosure()

x()--nil, 20
x() --21, 31
x() --32, 42
    --43, 53 -- if we iterated x() again

i risultati:

nil
20
31
42

Può diventare complicato, e probabilmente varia da lingua a lingua, ma in Lua sembra che ogni volta che viene eseguita una funzione, il suo stato viene ripristinato. Dico questo perché i risultati del codice sopra sarebbero diversi se accedessimo direttamente alla myclosurefunzione / stato (invece che attraverso la funzione anonima che ritorna), poiché pvalueverremmo ripristinati a 10; ma se accediamo allo stato di myclosure tramite x (la funzione anonima) puoi vedere che pvalueè vivo e vegeto da qualche parte nella memoria. Ho il sospetto che ci sia qualcosa in più, forse qualcuno può spiegare meglio la natura dell'implementazione.

PS: Non conosco una leccata di C ++ 11 (diversa da quella delle versioni precedenti), quindi nota che questo non è un confronto tra le chiusure in C ++ 11 e Lua. Inoltre, tutte le "linee tracciate" da Lua a C ++ sono somiglianze in quanto le variabili statiche e le chiusure non sono uguali al 100%; anche se a volte vengono utilizzati per risolvere problemi simili.

La cosa di cui non sono sicuro è, nell'esempio di codice sopra, se la funzione anonima o la funzione di ordine superiore è considerata la chiusura?


4

Una chiusura è una funzione che ha stato associato:

In perl crei chiusure come questa:

#!/usr/bin/perl

# This function creates a closure.
sub getHelloPrint
{
    # Bind state for the function we are returning.
    my ($first) = @_;a

    # The function returned will have access to the variable $first
    return sub { my ($second) = @_; print  "$first $second\n"; };
}

my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");

&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World

Se esaminiamo le nuove funzionalità fornite con C ++.
Inoltre, consente di associare lo stato corrente all'oggetto:

#include <string>
#include <iostream>
#include <functional>


std::function<void(std::string const&)> getLambda(std::string const& first)
{
    // Here we bind `first` to the function
    // The second parameter will be passed when we call the function
    return [first](std::string const& second) -> void
    {   std::cout << first << " " << second << "\n";
    };
}

int main(int argc, char* argv[])
{
    auto hw = getLambda("Hello");
    auto gw = getLambda("GoodBye");

    hw("World");
    gw("World");
}

2

Consideriamo una semplice funzione:

function f1(x) {
    // ... something
}

Questa funzione è chiamata funzione di livello superiore perché non è nidificata in nessun'altra funzione. Ogni funzione JavaScript associa a se stessa un elenco di oggetti chiamato "Scope Chain" . Questa catena di ambiti è un elenco ordinato di oggetti. Ognuno di questi oggetti definisce alcune variabili.

Nelle funzioni di livello superiore, la catena dell'ambito è costituita da un singolo oggetto, l'oggetto globale. Ad esempio, la funzione f1sopra ha una catena di ambito che contiene un singolo oggetto che definisce tutte le variabili globali. (nota che qui il termine "oggetto" non significa oggetto JavaScript, è solo un oggetto definito dall'implementazione che funge da contenitore di variabili, in cui JavaScript può "cercare" le variabili.)

Quando viene invocata questa funzione, JavaScript crea qualcosa chiamato "Oggetto di attivazione" e lo mette in cima alla catena dell'ambito. Questo oggetto contiene tutte le variabili locali (ad esempio xqui). Quindi ora abbiamo due oggetti nella catena dell'ambito: il primo è l'oggetto di attivazione e sotto di esso è l'oggetto globale.

Notare con molta attenzione che i due oggetti vengono inseriti nella catena dell'oscilloscopio in tempi DIVERSI. L'oggetto globale viene inserito quando viene definita la funzione (ovvero quando JavaScript ha analizzato la funzione e creato l'oggetto funzione) e l'oggetto di attivazione entra quando viene invocata la funzione.

Quindi ora sappiamo questo:

  • Ad ogni funzione è associata una catena di portata
  • Quando viene definita la funzione (quando viene creato l'oggetto funzione), JavaScript salva una catena di ambito con quella funzione
  • Per le funzioni di livello superiore, la catena dell'ambito contiene solo l'oggetto globale al momento della definizione della funzione e aggiunge un oggetto di attivazione aggiuntivo in cima al momento della chiamata

La situazione diventa interessante quando abbiamo a che fare con funzioni nidificate. Quindi, creiamo uno:

function f1(x) {

    function f2(y) {
        // ... something
    }

}

Quando f1viene definito otteniamo una catena di ambito per esso contenente solo l'oggetto globale.

Ora quando f1viene chiamato, la catena dell'ambito f1ottiene l'oggetto di attivazione. Questo oggetto di attivazione contiene la variabile xe la variabile f2che è una funzione. E, nota che f2si sta definendo. Quindi, a questo punto, JavaScript salva anche una nuova catena di ambito per f2. La catena di portata salvata per questa funzione interna è la catena di portata corrente in vigore. L'attuale catena di portata in effetti è quella di f1's. Quindi f2la catena di portata è f1la catena di portata corrente - che contiene l'oggetto di attivazione f1e l'oggetto globale.

Quando f2viene chiamato, ottiene il proprio oggetto di attivazione contenente y, aggiunto alla sua catena di portata che contiene già l'oggetto di attivazione f1e l'oggetto globale.

Se all'interno fosse definita un'altra funzione nidificata f2, la catena dell'ambito conterrebbe tre oggetti al momento della definizione (2 oggetti di attivazione di due funzioni esterne e l'oggetto globale) e 4 al momento dell'invocazione.

Quindi, ora capiamo come funziona la catena dell'ambito ma non abbiamo ancora parlato di chiusure.

La combinazione di un oggetto funzione e un ambito (un insieme di associazioni di variabili) in cui vengono risolte le variabili della funzione è chiamata chiusura nella letteratura informatica - JavaScript la guida definitiva di David Flanagan

La maggior parte delle funzioni viene invocata utilizzando la stessa catena di ambito che era in vigore al momento della definizione della funzione e non importa che sia coinvolta una chiusura. Le chiusure diventano interessanti quando vengono invocate in una catena di ambito diversa da quella che era in vigore al momento della definizione. Ciò accade più comunemente quando un oggetto funzione nidificato viene restituito dalla funzione in cui è stato definito.

Quando la funzione ritorna, quell'oggetto di attivazione viene rimosso dalla catena dell'ambito. Se non c'erano funzioni nidificate, non ci sono più riferimenti all'oggetto di attivazione e viene raccolta spazzatura. Se sono state definite funzioni nidificate, ciascuna di queste funzioni ha un riferimento alla catena dell'ambito e tale catena dell'ambito si riferisce all'oggetto di attivazione.

Se quegli oggetti con funzioni nidificate sono rimasti all'interno della loro funzione esterna, tuttavia, essi stessi saranno raccolti in modo inutile, insieme all'oggetto di attivazione a cui si riferivano. Ma se la funzione definisce una funzione nidificata e la restituisce o la memorizza in una proprietà da qualche parte, allora ci sarà un riferimento esterno alla funzione nidificata. Non sarà garbage collection e nemmeno l'oggetto di attivazione a cui fa riferimento non sarà garbage collection.

Nel nostro esempio precedente, non torniamo f2da f1, quindi, quando viene f1restituita una chiamata , il suo oggetto di attivazione verrà rimosso dalla sua catena di portata e i rifiuti raccolti. Ma se avessimo qualcosa del genere:

function f1(x) {

    function f2(y) {
        // ... something
    }

    return f2;
}

Qui, il ritorno f2avrà una catena di ambito che conterrà l'oggetto di attivazione di f1, e quindi non verrà raccolto. A questo punto, se chiamiamo f2, sarà in grado di accedere alla f1variabile xanche se siamo fuori f1.

Quindi possiamo vedere che una funzione mantiene la sua catena di portata con essa e con la catena di portata arrivano tutti gli oggetti di attivazione delle funzioni esterne. Questa è l'essenza della chiusura. Diciamo che le funzioni in JavaScript sono "con ambito lessicale" , il che significa che salvano l'ambito che era attivo quando sono state definite rispetto all'ambito che era attivo quando sono state chiamate.

Esistono numerose potenti tecniche di programmazione che comportano chiusure quali approssimazioni di variabili private, programmazione guidata da eventi, applicazione parziale , ecc.

Si noti inoltre che tutto ciò si applica a tutte quelle lingue che supportano le chiusure. Ad esempio PHP (5.3+), Python, Ruby, ecc.


-1

Una chiusura è un'ottimizzazione del compilatore (aka zucchero sintattico?). Alcune persone si sono riferite a questo anche come Poor Man's Object .

Vedi la risposta di Eric Lippert : (estratto sotto)

Il compilatore genererà il codice in questo modo:

private class Locals
{
  public int count;
  public void Anonymous()
  {
    this.count++;
  }
}

public Action Counter()
{
  Locals locals = new Locals();
  locals.count = 0;
  Action counter = new Action(locals.Anonymous);
  return counter;
}

Ha senso?
Inoltre, hai chiesto confronti. VB e JScript creano entrambi chiusure praticamente allo stesso modo.


Questa risposta è un CW perché non merito punti per la grande risposta di Eric. Per favore, votalo come ritieni opportuno. HTH
goodguys_activate il

3
-1: la tua spiegazione è troppo radicale in C #. La chiusura è usata in molte lingue ed è molto più dello zucchero sintattico in queste lingue e comprende sia la funzione che lo stato.
Martin York,

1
No, una chiusura non è né solo una "ottimizzazione del compilatore" né zucchero sintattico. -1
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.