¿Qué es un cierre?

155

De vez en cuando veo que se mencionan los "cierres", e intenté buscarlo, pero Wiki no da una explicación que entiendo. ¿Podría alguien ayudarme aquí?

gablin
fuente
Si conoces Java / C # espero Este enlace se ayuda- http://www.developerfusion.com/article/8251/the-beauty-of-closures/
Gulshan
1
Los cierres son difíciles de entender. Debería intentar hacer clic en todos los enlaces en la primera oración de ese artículo de Wikipedia y comprender primero esos artículos.
Zach
3
Sin embargo, ¿cuál es la diferencia fundamental entre un cierre y una clase? De acuerdo, una clase con un solo método público.
biziclop
55
@biziclop: Usted podría emular a un cierre con una clase (eso es lo que los desarrolladores de Java tienen que hacer). Pero por lo general, son un poco menos detallados para crear y no tiene que administrar manualmente lo que está cargando. (Los lispers hardcore hacen una pregunta similar, pero llegan a esa otra conclusión: que el soporte de OO a nivel de lenguaje es innecesario cuando hay cierres).

Respuestas:

141

(Descargo de responsabilidad: esta es una explicación básica; en lo que respecta a la definición, estoy simplificando un poco)

La forma más simple de pensar en un cierre es una función que se puede almacenar como una variable (denominada "función de primera clase"), que tiene una capacidad especial para acceder a otras variables locales en el ámbito en el que se creó.

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

Las funciones 1 asignadas document.onclicky displayValOfBlackson cierres. Puede ver que ambos hacen referencia a la variable booleana black, pero esa variable se asigna fuera de la función. Como blackes local en el ámbito donde se definió la función , se conserva el puntero a esta variable.

Si pones esto en una página HTML:

  1. Haga clic para cambiar a negro
  2. Presiona [enter] para ver "verdadero"
  3. Haga clic de nuevo, cambia a blanco
  4. Presione [enter] para ver "falso"

Esto demuestra que ambos tienen acceso al mismo black y pueden usarse para almacenar el estado sin ningún objeto contenedor.

La llamada a setKeyPresses demostrar cómo se puede pasar una función como cualquier variable. El alcance conservado en el cierre sigue siendo aquel en el que se definió la función.

Los cierres se usan comúnmente como controladores de eventos, especialmente en JavaScript y ActionScript. El buen uso de los cierres lo ayudará a vincular implícitamente las variables a los controladores de eventos sin tener que crear un contenedor de objetos. Sin embargo, el uso descuidado conducirá a pérdidas de memoria (como cuando un controlador de eventos no utilizado pero preservado es lo único que puede retener objetos grandes en la memoria, especialmente objetos DOM, evitando la recolección de basura).


1: En realidad, todas las funciones en JavaScript son cierres.

Nicole
fuente
3
Mientras leía tu respuesta, sentí una bombilla encenderse en mi mente. ¡Muy apreciado! :)
Jay
1
Como blackse declara dentro de una función, ¿no se destruiría eso a medida que la pila se desenrolla ...?
gablin
1
@gablin, eso es lo único de los idiomas que tienen cierres. Todos los idiomas con recolección de basura funcionan de la misma manera: cuando no se mantienen más referencias a un objeto, se puede destruir. Cada vez que se crea una función en JS, el ámbito local está vinculado a esa función hasta que se destruya esa función.
Nicole
2
@gablin, esa es una buena pregunta. No creo que puedan y mdash; pero solo mencioné la recolección de basura, ya que eso es lo que usa JS y a eso parecía referirse cuando dijo "Dado que blackse declara dentro de una función, eso no se destruiría". Recuerde también que si declara un objeto en una función y luego lo asigna a una variable que vive en otro lugar, ese objeto se conserva porque hay otras referencias a él.
Nicole
1
Objective-C (y C bajo clang) admite bloques, que son esencialmente cierres, sin recolección de basura. Requiere soporte de tiempo de ejecución y alguna intervención manual en torno a la administración de memoria.
Quixoto
68

Un cierre es básicamente una forma diferente de mirar un objeto. Un objeto son datos que tienen una o más funciones vinculadas. Un cierre es una función que tiene una o más variables vinculadas. Los dos son básicamente idénticos, al menos en un nivel de implementación. La verdadera diferencia está en de dónde vienen.

En la programación orientada a objetos, declara una clase de objeto definiendo sus variables miembro y sus métodos (funciones miembro) por adelantado, y luego crea instancias de esa clase. Cada instancia viene con una copia de los datos del miembro, inicializada por el constructor. Luego tiene una variable de un tipo de objeto y la pasa como un dato, porque el foco está en su naturaleza como dato.

En un cierre, por otro lado, el objeto no se define por adelantado como una clase de objeto, ni se instancia a través de una llamada de constructor en su código. En cambio, escribe el cierre como una función dentro de otra función. El cierre puede referirse a cualquiera de las variables locales de la función externa, y el compilador lo detecta y mueve estas variables desde el espacio de la pila de la función externa a la declaración de objeto oculto del cierre. Luego tiene una variable de tipo de cierre, y aunque es básicamente un objeto debajo del capó, la pasa como referencia de función, porque el foco está en su naturaleza como función.

Mason Wheeler
fuente
3
+1: buena respuesta. Puede ver un cierre como un objeto con un solo método, y un objeto arbitrario como una colección de cierres sobre algunos datos subyacentes comunes (las variables miembro del objeto). Creo que estas dos vistas son bastante simétricas.
Giorgio el
3
Muy buena respuesta. En realidad, explica la idea del cierre.
RoboAlex
1
@Mason Wheeler: ¿Dónde se almacenan los datos de cierre? En la pila como una función? ¿O en el montón como un objeto?
RoboAlex
1
@RoboAlex: en el montón, porque es un objeto que parece una función.
Mason Wheeler
1
@RoboAlex: el lugar donde se almacena un cierre y sus datos capturados depende de la implementación. En C ++ se puede almacenar en el montón o en la pila.
Giorgio
29

El término cierre proviene del hecho de que un fragmento de código (bloque, función) puede tener variables libres que están cerradas (es decir, vinculadas a un valor) por el entorno en el que se define el bloque de código.

Tomemos, por ejemplo, la definición de la función Scala:

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

En el cuerpo de la función hay dos nombres (variables) vy kque indican dos valores enteros. El nombre vestá enlazado porque se declara como un argumento de la función addConstant(al observar la declaración de la función sabemos que vse le asignará un valor cuando se invoque la función). El nombre kes libre de la función addConstantporque la función no contiene ninguna pista sobre qué valor kestá vinculado (y cómo).

Para evaluar una llamada como:

val n = addConstant(10)

tenemos que asignar kun valor, que solo puede suceder si el nombre kse define en el contexto en el que addConstantse define. Por ejemplo:

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

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

  values.map(addConstant)
}

Ahora que lo hemos definido addConstanten un contexto donde kestá definido, se addConstantha convertido en un cierre porque todas sus variables libres ahora están cerradas (vinculadas a un valor): addConstantpueden invocarse y pasarse como si fuera una función. Tenga en cuenta que la variable libre kestá vinculada a un valor cuando se define el cierre , mientras que la variable de argumento vestá vinculada cuando se invoca el cierre .

Por lo tanto, un cierre es básicamente una función o un bloque de código que puede acceder a valores no locales a través de sus variables libres después de que estos hayan sido vinculados por el contexto.

En muchos idiomas, si usa un cierre solo una vez, puede hacerlo anónimo , p. Ej.

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

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

Tenga en cuenta que una función sin variables libres es un caso especial de cierre (con un conjunto vacío de variables libres). Análogamente, una función anónima es un caso especial de un cierre anónimo , es decir, una función anónima es un cierre anónimo sin variables libres.

Giorgio
fuente
Esto concuerda bien con las fórmulas cerradas y abiertas en la lógica. Gracias por tu respuesta.
RainDoctor
@RainDoctor: las variables libres se definen en fórmulas lógicas y en expresiones de cálculo lambda de manera similar: el lambda en una expresión lambda funciona como un cuantificador en las fórmulas lógicas wrt variables libres / enlazadas.
Giorgio
9

Una explicación simple en 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)utilizará el valor creado previamente de closure. El alertValueespacio de nombres de la función devuelta se conectará al espacio de nombres en el que closurereside la variable. Cuando elimine la función completa, el valor de la closurevariable se eliminará, pero hasta entonces, la alertValuefunción siempre podrá leer / escribir el valor de la variable closure.

Si ejecuta este código, la primera iteración asignará un valor 0 a la closurevariable y reescribirá la función para:

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

Y debido a que alertValuenecesita la variable local closurepara ejecutar la función, se vincula con el valor de la variable local previamente asignada closure.

Y ahora cada vez que llame a la closure_examplefunción, escribirá el valor incrementado de la closurevariable porque alert(closure)está enlazado.

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 
Muha
fuente
gracias, no probé el código =) todo parece estar bien ahora.
Muha
5

Un "cierre" es, en esencia, un estado local y un código, combinados en un paquete. Por lo general, el estado local proviene de un ámbito circundante (léxico) y el código es (esencialmente) una función interna que luego se devuelve al exterior. El cierre es entonces una combinación de las variables capturadas que ve la función interna y el código de la función interna.

Es una de esas cosas que, desafortunadamente, es un poco difícil de explicar, debido a que no está familiarizado.

Una analogía que utilicé con éxito en el pasado fue "imagina que tenemos algo que llamamos 'el libro', en el cierre de la habitación, 'el libro' es esa copia allí, en la esquina, de TAOCP, pero en el cierre de la mesa , es esa copia de un libro de Dresden Files. Entonces, dependiendo del cierre en el que se encuentre, el código 'dame el libro' da como resultado que sucedan diferentes cosas ".

Vatine
fuente
Olvidó esto: en.wikipedia.org/wiki/Closure_(computer_programming) en su respuesta.
S.Lott
3
No, decidí no cerrar esa página.
Vatine
"Estado y función": ¿Puede una función C con una staticvariable local considerarse un cierre? ¿Los cierres en Haskell involucran al estado?
Giorgio el
2
@Giorgio Closures en Haskell cierra (creo) sobre los argumentos en el ámbito léxico en el que están definidos, así que diría "sí" (aunque en el mejor de los casos no estoy familiarizado con Haskell). La función de CA con una variable estática es, en el mejor de los casos, un cierre muy limitado (realmente desea poder crear múltiples cierres desde una sola función, con una staticvariable local, tiene exactamente uno).
Vatine
Hice esta pregunta a propósito porque creo que una función C con una variable estática no es un cierre: la variable estática se define localmente y solo se conoce dentro del cierre, no tiene acceso al entorno. Además, no estoy 100% seguro, pero formularía su declaración al revés: utiliza el mecanismo de cierre para crear diferentes funciones (una función es una definición de cierre + un enlace para sus variables libres).
Giorgio el
5

Es difícil definir qué es el cierre sin definir el concepto de "estado".

Básicamente, en un lenguaje con alcance léxico completo que trata las funciones como valores de primera clase, sucede algo especial. Si tuviera que hacer algo como:

function foo(x)
return x
end

x = foo

La variable xno solo hace referencia function foo()sino que también hace referencia al estado que foose dejó la última vez que regresó. La verdadera magia ocurre cuando footiene otras funciones más definidas dentro de su alcance; es como su propio mini-entorno (tal como 'normalmente' definimos funciones en un entorno global).

Funcionalmente, puede resolver muchos de los mismos problemas que la palabra clave 'estática' de C ++ (C?), Que retiene el estado de una variable local a través de múltiples llamadas de función; sin embargo, es más como aplicar ese mismo principio (variable estática) a una función, ya que las funciones son valores de primera clase; el cierre agrega soporte para que se guarde todo el estado de la función (nada que ver con las funciones estáticas de C ++).

Tratar las funciones como valores de primera clase y agregar soporte para cierres también significa que puede tener más de una instancia de la misma función en la memoria (similar a las clases). Lo que esto significa es que puede reutilizar el mismo código sin tener que restablecer el estado de la función, como se requiere cuando se trata con variables estáticas de C ++ dentro de una función (¿puede estar equivocado sobre esto?).

Aquí hay algunas pruebas del soporte de cierre de 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

resultados:

nil
20
31
42

Puede ser complicado, y probablemente varía de un idioma a otro, pero en Lua parece que cada vez que se ejecuta una función, se restablece su estado. Digo esto porque los resultados del código anterior serían diferentes si estuviéramos accediendo a la myclosurefunción / estado directamente (en lugar de a través de la función anónima que devuelve), ya pvalueque se restablecería a 10; pero si accedemos al estado de myclosure a través de x (la función anónima) puede ver que pvalueestá vivo y bien en algún lugar de la memoria. Sospecho que hay un poco más, tal vez alguien pueda explicar mejor la naturaleza de la implementación.

PD: No conozco ni una pizca de C ++ 11 (aparte de lo que hay en versiones anteriores), así que tenga en cuenta que esto no es una comparación entre los cierres en C ++ 11 y Lua. Además, todas las 'líneas dibujadas' de Lua a C ++ son similitudes ya que las variables estáticas y los cierres no son 100% iguales; incluso si a veces se usan para resolver problemas similares.

Lo que no estoy seguro es, en el ejemplo de código anterior, si la función anónima o la función de orden superior se considera el cierre.

Trae Barlow
fuente
4

Un cierre es una función que tiene un estado asociado:

En perl creas cierres como este:

#!/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

Si nos fijamos en la nueva funcionalidad proporcionada con C ++.
También le permite vincular el estado actual al objeto:

#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");
}
Martin York
fuente
2

Consideremos una función simple:

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

Esta función se llama función de nivel superior porque no está anidada en ninguna otra función. Cada función de JavaScript asocia consigo una lista de objetos llamada "Cadena de alcance" . Esta cadena de alcance es una lista ordenada de objetos. Cada uno de estos objetos define algunas variables.

En las funciones de nivel superior, la cadena de alcance consta de un solo objeto, el objeto global. Por ejemplo, la función f1anterior tiene una cadena de alcance que tiene un solo objeto que define todas las variables globales. (tenga en cuenta que el término "objeto" aquí no significa objeto JavaScript, es solo un objeto definido por la implementación que actúa como un contenedor de variables, en el que JavaScript puede "buscar" variables).

Cuando se invoca esta función, JavaScript crea algo llamado "objeto de activación" y lo coloca en la parte superior de la cadena de alcance. Este objeto contiene todas las variables locales (por ejemplo, xaquí). Por lo tanto, ahora tenemos dos objetos en la cadena de alcance: el primero es el objeto de activación y debajo está el objeto global.

Tenga en cuenta con mucho cuidado que los dos objetos se colocan en la cadena del osciloscopio en DIFERENTES veces. El objeto global se coloca cuando se define la función (es decir, cuando JavaScript analiza la función y crea el objeto de la función), y el objeto de activación entra cuando se invoca la función.

Entonces, ahora sabemos esto:

  • Cada función tiene una cadena de alcance asociada
  • Cuando se define la función (cuando se crea el objeto de función), JavaScript guarda una cadena de alcance con esa función
  • Para funciones de nivel superior, la cadena de alcance contiene solo el objeto global en el momento de definición de la función y agrega un objeto de activación adicional en la parte superior en el momento de invocación

La situación se pone interesante cuando tratamos con funciones anidadas. Entonces, creemos uno:

function f1(x) {

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

}

Cuando f1se define, obtenemos una cadena de alcance que contiene solo el objeto global.

Ahora cuando f1se llama, la cadena de alcance de f1obtiene el objeto de activación. Este objeto de activación contiene la variable xy la variable f2que es una función. Y, tenga en cuenta que f2se está definiendo. Por lo tanto, en este punto, JavaScript también guarda una nueva cadena de alcance para f2. La cadena de alcance guardada para esta función interna es la cadena de alcance actual vigente. La cadena de alcance actual en efecto es la de f1's. Por lo tanto f2, la cadena de alcance es f1la cadena de alcance actual , que contiene el objeto de activación f1y el objeto global.

Cuando f2se llama, obtiene su propio objeto de activación que contiene y, agregado a su cadena de alcance que ya contiene el objeto de activación f1y el objeto global.

Si hubiera otra función anidada definida dentro f2, su cadena de alcance contendría tres objetos en el momento de la definición (2 objetos de activación de dos funciones externas y el objeto global) y 4 en el momento de la invocación.

Entonces, ahora entendemos cómo funciona la cadena de alcance, pero aún no hemos hablado de cierres.

La combinación de un objeto de función y un ámbito (un conjunto de enlaces de variables) en el que se resuelven las variables de la función se denomina cierre en la literatura de informática - JavaScript, la guía definitiva de David Flanagan

La mayoría de las funciones se invocan usando la misma cadena de alcance que estaba vigente cuando se definió la función, y realmente no importa que haya un cierre involucrado. Los cierres se vuelven interesantes cuando se invocan bajo una cadena de alcance diferente a la que estaba vigente cuando se definieron. Esto ocurre más comúnmente cuando un objeto de función anidada se devuelve desde la función dentro de la cual se definió.

Cuando la función regresa, ese objeto de activación se elimina de la cadena de alcance. Si no había funciones anidadas, no hay más referencias al objeto de activación y se recolecta basura. Si se definieron funciones anidadas, cada una de esas funciones tiene una referencia a la cadena de alcance, y esa cadena de alcance se refiere al objeto de activación.

Sin embargo, si esos objetos de funciones anidadas permanecieron dentro de su función externa, entonces ellos mismos serán recolectados de basura, junto con el objeto de activación al que se refirieron. Pero si la función define una función anidada y la devuelve o la almacena en una propiedad en algún lugar, entonces habrá una referencia externa a la función anidada. No se recolectará basura, y el objeto de activación al que hace referencia tampoco se recolectará basura.

En nuestro ejemplo anterior, no regresamos f2de f1, por lo tanto, cuando una llamada a f1retorna, su objeto de activación se eliminará de su cadena de alcance y se recolectará la basura. Pero si tuviéramos algo como esto:

function f1(x) {

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

    return f2;
}

Aquí, la devolución f2tendrá una cadena de alcance que contendrá el objeto de activación f1y, por lo tanto, no se recolectará basura. En este punto, si llamamos f2, podrá acceder a f1la variable xaunque estemos fuera de f1.

Por lo tanto, podemos ver que una función mantiene su cadena de alcance con ella y con la cadena de alcance vienen todos los objetos de activación de funciones externas. Esta es la esencia del cierre. Decimos que las funciones en JavaScript tienen "ámbito léxico" , lo que significa que guardan el ámbito que estaba activo cuando se definieron en lugar del ámbito que estaba activo cuando se les llamaba.

Hay una serie de potentes técnicas de programación que implican cierres como aproximación de variables privadas, programación dirigida por eventos, aplicación parcial , etc.

También tenga en cuenta que todo esto se aplica a todos los idiomas que admiten cierres. Por ejemplo PHP (5.3+), Python, Ruby, etc.

codificador de árboles
fuente
-1

Un cierre es una optimización del compilador (también conocido como azúcar sintáctico?). Algunas personas también se han referido a esto como el Objeto del Pobre .

Ver la respuesta de Eric Lippert : (extracto a continuación)

El compilador generará código como este:

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

¿Tener sentido?
Además, solicitó comparaciones. VB y JScript crean cierres de la misma manera.

LamonteCristo
fuente
Esta respuesta es una CW porque no merezco puntos por la gran respuesta de Eric. Por favor, vótelo como mejor le parezca. HTH
goodguys_activate
3
-1: Su explicación es demasiado raíz en C #. El cierre se usa en muchos idiomas y es mucho más que azúcar sintáctica en estos idiomas y abarca tanto la función como el estado.
Martin York
1
No, un cierre no es solo una "optimización del compilador" ni un azúcar sintáctico. -1