O que é um fechamento?

155

De vez em quando eu vejo "fechamentos" sendo mencionados, e tentei procurar, mas o Wiki não dá uma explicação que eu entenda. Alguém poderia me ajudar aqui?

gablin
fonte
Se você sabe Java / C # espero que esta ligação irá ajuda- http://www.developerfusion.com/article/8251/the-beauty-of-closures/
Gulshan
1
É difícil entender os fechamentos. Você deve tentar clicar em todos os links na primeira frase desse artigo da Wikipedia e entender esses artigos primeiro.
Zach
3
Qual é a diferença fundamental entre um encerramento e uma aula? Ok, uma classe com apenas um método público.
biziclop 27/01
5
@ BizClop: Você pode emular um fechamento com uma classe (é isso que os desenvolvedores Java precisam fazer). Mas eles geralmente são um pouco menos detalhados para criar e você não precisa gerenciar manualmente o que está levando. (Os covardes hardcore fazem uma pergunta semelhante, mas é provável que cheguem a essa outra conclusão - que o suporte ao OO no nível da linguagem é desnecessário quando você tem fechamentos).

Respostas:

141

(Isenção de responsabilidade: esta é uma explicação básica; no que diz respeito à definição, estou simplificando um pouco)

A maneira mais simples de pensar em um fechamento é uma função que pode ser armazenada como uma variável (denominada "função de primeira classe"), que possui uma capacidade especial de acessar outras variáveis ​​locais do escopo em que foi criado.

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

As funções 1 atribuídas document.onclicke displayValOfBlacksão encerramentos. Você pode ver que os dois referenciam a variável booleana black, mas essa variável é atribuída fora da função. Como blacké local no escopo em que a função foi definida , o ponteiro para essa variável é preservado.

Se você colocar isso em uma página HTML:

  1. Clique para mudar para preto
  2. Pressione [enter] para ver "true"
  3. Clique novamente, muda novamente para branco
  4. Pressione [enter] para ver "false"

Isso demonstra que ambos têm acesso ao mesmo black e podem ser usados ​​para armazenar o estado sem nenhum objeto de wrapper.

A chamada para setKeyPressé demonstrar como uma função pode ser passada como qualquer variável. O escopo preservado no fechamento ainda é aquele em que a função foi definida.

Os fechamentos são comumente usados ​​como manipuladores de eventos, especialmente em JavaScript e ActionScript. O bom uso de encerramentos ajudará você a vincular implicitamente variáveis ​​a manipuladores de eventos sem precisar criar um wrapper de objeto. No entanto, o uso descuidado levará a vazamentos de memória (como quando um manipulador de eventos não utilizado, mas preservado, é a única coisa a manter objetos grandes na memória, especialmente objetos DOM, impedindo a coleta de lixo).


1: Na verdade, todas as funções em JavaScript são encerramentos.

Nicole
fonte
3
Enquanto lia sua resposta, senti uma lâmpada acender em minha mente. Muito apreciado! :)
Jay
1
Como blacké declarado dentro de uma função, isso não seria destruído à medida que a pilha se desenrola ...?
gablin
1
@ gablin, é o que há de único nos idiomas que têm encerramentos. Todos os idiomas com coleta de lixo funcionam da mesma maneira - quando não há mais referências a um objeto, ele pode ser destruído. Sempre que uma função é criada em JS, o escopo local é vinculado a essa função até que essa função seja destruída.
Nicole
2
@ Gablin, essa é uma boa pergunta. Eu não acho que eles não possam & mdash; mas eu só trouxe a coleta de lixo desde então, o que JS usa e é a isso que você parecia estar se referindo quando disse "Como blacké declarado dentro de uma função, isso não seria destruído". Lembre-se também de que, se você declarar um objeto em uma função e depois atribuí-lo a uma variável que mora em outro lugar, esse objeto será preservado porque há outras referências a ele.
Nicole
1
O Objective-C (e C sob clang) suporta blocos, que são essencialmente fechamentos, sem coleta de lixo. Requer suporte de tempo de execução e alguma intervenção manual em torno do gerenciamento de memória.
Quixoto 06/11/12
68

Um fechamento é basicamente apenas uma maneira diferente de olhar para um objeto. Um objeto são dados que possuem uma ou mais funções vinculadas a ele. Um fechamento é uma função que possui uma ou mais variáveis ​​ligadas a ele. Os dois são basicamente idênticos, pelo menos em um nível de implementação. A verdadeira diferença está em onde eles vêm.

Na programação orientada a objetos, você declara uma classe de objeto definindo suas variáveis ​​de membro e seus métodos (funções de membro) antecipadamente e, em seguida, cria instâncias dessa classe. Cada instância vem com uma cópia dos dados do membro, inicializada pelo construtor. Você então tem uma variável de um tipo de objeto e a transmite como um dado, porque o foco é sua natureza como dados.

Em um fechamento, por outro lado, o objeto não é definido antecipadamente como uma classe de objeto ou instanciado por meio de uma chamada de construtor em seu código. Em vez disso, você escreve o fechamento como uma função dentro de outra função. O fechamento pode se referir a qualquer variável local da função externa e o compilador detecta isso e move essas variáveis ​​do espaço de pilha da função externa para a declaração de objeto oculto do fechamento. Você então tem uma variável de um tipo de fechamento e, embora seja basicamente um objeto sob o capô, transmite-o como uma referência de função, porque o foco é sua natureza como uma função.

Mason Wheeler
fonte
3
+1: boa resposta. Você pode ver um fechamento como um objeto com apenas um método e um objeto arbitrário como uma coleção de fechamentos sobre alguns dados subjacentes comuns (as variáveis ​​de membro do objeto). Eu acho que essas duas visões são bastante simétricas.
Giorgio
3
Resposta muito boa. Na verdade, explica o insight do fechamento.
RoboAlex #
1
@Mason Wheeler: Onde os dados de fechamento são armazenados? Na pilha como uma função? Ou na pilha como um objeto?
RoboAlex #
1
@RoboAlex: Na pilha, porque é um objeto que se parece com uma função.
Mason Wheeler
1
@RoboAlex: onde um fechamento e seus dados capturados são armazenados, depende da implementação. No C ++, ele pode ser armazenado na pilha ou na pilha.
Giorgio
29

O termo encerramento vem do fato de que um trecho de código (bloco, função) pode ter variáveis ​​livres que são fechadas (ou seja, vinculadas a um valor) pelo ambiente em que o bloco de código está definido.

Tomemos, por exemplo, a definição da função Scala:

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

No corpo da função, existem dois nomes (variáveis) ve kindicam dois valores inteiros. O nome vé vinculado porque é declarado como um argumento da função addConstant(observando a declaração da função, sabemos que vserá atribuído um valor quando a função for chamada). O nome ké livre para a função addConstantporque a função não contém nenhuma pista sobre a qual valor kestá vinculado (e como).

Para avaliar uma chamada como:

val n = addConstant(10)

temos que atribuir kum valor, o que só pode acontecer se o nome kestiver definido no contexto em que addConstantestá definido. Por exemplo:

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

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

  values.map(addConstant)
}

Agora que definimos addConstantem um contexto em que ké definido, addConstanttornou-se um fechamento porque todas as suas variáveis ​​livres agora estão fechadas (vinculadas a um valor): addConstantpodem ser invocadas e passadas como se fosse uma função. Observe que a variável livre ké vinculada a um valor quando o fechamento é definido , enquanto a variável de argumento vé vinculada quando o fechamento é invocado .

Portanto, um fechamento é basicamente uma função ou bloco de código que pode acessar valores não locais por meio de suas variáveis ​​livres depois que elas são vinculadas pelo contexto.

Em muitos idiomas, se você usar um fechamento apenas uma vez, poderá torná-lo anônimo , por exemplo

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

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

Observe que uma função sem variáveis ​​livres é um caso especial de fechamento (com um conjunto vazio de variáveis ​​livres). Analogamente, uma função anônima é um caso especial de fechamento anônimo , ou seja, uma função anônima é um fechamento anônimo sem variáveis ​​livres.

Giorgio
fonte
Isso combina bem com fórmulas fechadas e abertas na lógica. Obrigado pela sua resposta.
RainDoctor 23/09
@RainDoctor: Variáveis ​​livres são definidas em fórmulas lógicas e em expressões de cálculo lambda de maneira semelhante: o lambda em uma expressão lambda funciona como um quantificador em fórmulas lógicas por variáveis ​​livres / limitadas.
Giorgio
9

Uma explicação simples em 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)usará o valor criado anteriormente de closure. O alertValueespaço de nome da função retornada será conectado ao espaço de nome no qual a closurevariável reside. Quando você exclui toda a função, o valor da closurevariável será excluído, mas até então, a alertValuefunção sempre poderá ler / gravar o valor da variável closure.

Se você executar esse código, a primeira iteração atribuirá um valor 0 à closurevariável e reescreverá a função para:

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

E porque alertValueprecisa da variável local closurepara executar a função, ela se liga ao valor da variável local atribuída anteriormente closure.

E agora, toda vez que você chama a closure_examplefunção, ela grava o valor incrementado da closurevariável porque alert(closure)está vinculada.

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 
Muha
fonte
obrigado, eu não testei o código =) tudo parece bem agora.
Muha 28/01
5

Um "fechamento" é, em essência, algum estado local e algum código, combinados em um pacote. Normalmente, o estado local vem de um escopo circundante (lexical) e o código é (essencialmente) uma função interna que é retornada para fora. O fechamento é então uma combinação das variáveis ​​capturadas que a função interna vê e o código da função interna.

Infelizmente, é uma daquelas coisas que é um pouco difícil de explicar, por não ser familiar.

Uma analogia que usei com sucesso no passado foi "imagine que temos algo que chamamos de 'livro', no fechamento da sala, 'o livro' é aquela cópia ali, no canto, do TAOCP, mas no fechamento da mesa , é a cópia de um livro do Dresden Files. Portanto, dependendo de qual encerramento você está, o código 'me dê o livro' resulta em diferentes coisas acontecendo ".

Vatine
fonte
Você esqueceu o seguinte: en.wikipedia.org/wiki/Closure_(computer_programming) em sua resposta.
precisa saber é o seguinte
3
Não, eu escolhi conscientemente não fechar essa página.
Vatine
"Estado e função.": Uma função C com uma staticvariável local pode ser considerada um fechamento? Os fechamentos em Haskell envolvem estado?
Giorgio
2
@Giorgio Closures em Haskell (acredito) encerra os argumentos no escopo lexical em que estão definidos, então eu diria "sim" (embora eu não esteja familiarizado com Haskell). A função CA com uma variável estática é, na melhor das hipóteses, um fechamento muito limitado (você realmente deseja criar vários fechamentos a partir de uma única função, com uma staticvariável local, você tem exatamente um).
Vatine 7/11
Fiz essa pergunta de propósito porque acho que uma função C com uma variável estática não é um fechamento: a variável estática é definida localmente e conhecida apenas dentro do fechamento, não acessa o ambiente. Além disso, não tenho 100% de certeza, mas formularia sua declaração de maneira inversa: você usa o mecanismo de fechamento para criar funções diferentes (uma função é uma definição de fechamento + uma ligação para suas variáveis ​​livres).
Giorgio
5

É difícil definir o que é o fechamento sem definir o conceito de "estado".

Basicamente, em uma linguagem com escopo lexical completo que trata as funções como valores de primeira classe, algo especial acontece. Se eu fizesse algo como:

function foo(x)
return x
end

x = foo

A variável xnão apenas faz referência, function foo()mas também faz referência ao estado que foofoi deixado na última vez que retornou. A mágica real acontece quando foohá outras funções definidas mais dentro de seu escopo; é como seu próprio mini-ambiente (assim como 'normalmente' definimos funções em um ambiente global).

Funcionalmente, ele pode resolver muitos dos mesmos problemas que a palavra-chave 'estática' do C ++ (C?), Que retém o estado de uma variável local através de várias chamadas de função; no entanto, é mais como aplicar o mesmo princípio (variável estática) a uma função, pois as funções são valores de primeira classe; O encerramento adiciona suporte para que todo o estado da função seja salvo (nada a ver com as funções estáticas do C ++).

Tratar funções como valores de primeira classe e adicionar suporte a fechamentos também significa que você pode ter mais de uma instância da mesma função na memória (semelhante às classes). O que isso significa é que você pode reutilizar o mesmo código sem precisar redefinir o estado da função, conforme é necessário ao lidar com variáveis ​​estáticas do C ++ dentro de uma função (pode estar errado sobre isso?).

Aqui estão alguns testes do suporte ao fechamento 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

Pode ser complicado e provavelmente varia de idioma para idioma, mas parece em Lua que sempre que uma função é executada, seu estado é redefinido. Digo isso porque os resultados do código acima seriam diferentes se estivéssemos acessando a myclosurefunção / estado diretamente (em vez de retornar pela função anônima), pois pvalueseria redefinido para 10; mas se acessarmos o estado do myclosure por meio de x (a função anônima), você poderá ver que ele pvalueestá vivo e bom em algum lugar da memória. Suspeito que exista um pouco mais, talvez alguém possa explicar melhor a natureza da implementação.

PS: Eu não conheço uma lambida de C ++ 11 (além do que está nas versões anteriores), portanto, observe que essa não é uma comparação entre fechamentos em C ++ 11 e Lua. Além disso, todas as 'linhas desenhadas' de Lua para C ++ são semelhanças, pois variáveis ​​estáticas e fechamentos não são 100% iguais; mesmo que sejam usados ​​algumas vezes para resolver problemas semelhantes.

O que eu não tenho certeza é, no exemplo de código acima, se a função anônima ou a função de ordem superior é considerada o fechamento?

Trae Barlow
fonte
4

Um fechamento é uma função que tem estado associado:

No perl, você cria fechamentos 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

Se olharmos para a nova funcionalidade fornecida com o C ++.
Também permite vincular o estado atual ao 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
fonte
2

Vamos considerar uma função simples:

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

Essa função é chamada de função de nível superior porque não está aninhada em nenhuma outra função. Toda função JavaScript associa consigo uma lista de objetos chamada "Cadeia de escopo" . Essa cadeia de escopo é uma lista ordenada de objetos. Cada um desses objetos define algumas variáveis.

Nas funções de nível superior, a cadeia de escopo consiste em um único objeto, o objeto global. Por exemplo, a função f1acima possui uma cadeia de escopo que possui um único objeto que define todas as variáveis ​​globais. (observe que o termo "objeto" aqui não significa objeto JavaScript, é apenas um objeto definido para implementação que atua como um contêiner de variável, no qual o JavaScript pode "procurar" variáveis.)

Quando essa função é chamada, o JavaScript cria algo chamado "objeto de ativação" e o coloca no topo da cadeia de escopo. Este objeto contém todas as variáveis ​​locais (por exemplo, xaqui). Portanto, agora temos dois objetos na cadeia de escopo: o primeiro é o objeto de ativação e, abaixo dele, o objeto global.

Observe com muito cuidado que os dois objetos são colocados na cadeia de escopo em momentos DIFERENTES. O objeto global é colocado quando a função é definida (ou seja, quando o JavaScript analisa a função e cria o objeto da função) e o objeto de ativação entra quando a função é chamada.

Então, agora sabemos isso:

  • Toda função tem uma cadeia de escopo associada a ela
  • Quando a função é definida (quando o objeto da função é criado), o JavaScript salva uma cadeia de escopo com essa função
  • Para funções de nível superior, a cadeia de escopo contém apenas o objeto global no momento da definição da função e adiciona um objeto de ativação adicional na parte superior no momento da chamada

A situação fica interessante quando lidamos com funções aninhadas. Então, vamos criar um:

function f1(x) {

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

}

Quando f1definido, obtemos uma cadeia de escopo contendo apenas o objeto global.

Agora, quando f1é chamado, a cadeia de escopo f1recebe o objeto de ativação. Este objeto de ativação contém a variável xe a variável f2que é uma função. E observe que f2está sendo definido. Portanto, neste ponto, o JavaScript também salva uma nova cadeia de escopo f2. A cadeia de escopo salva para esta função interna é a atual cadeia de escopo em vigor. A cadeia de escopo atual em vigor é a de f1's. Portanto f2, a cadeia de escopo é f1a cadeia de escopo atual - que contém o objeto de ativação f1e o objeto global.

Quando f2é chamado, ele obtém seu próprio objeto de ativação y, adicionado à sua cadeia de escopo, que já contém o objeto de ativação f1e o objeto global.

Se houvesse outra função aninhada definida dentro f2, sua cadeia de escopo conteria três objetos no momento da definição (2 objetos de ativação de duas funções externas e o objeto global) e 4 no momento da chamada.

Então, agora entendemos como a cadeia de escopo funciona, mas ainda não falamos sobre fechamentos.

A combinação de um objeto de função e um escopo (um conjunto de ligações de variáveis) no qual as variáveis ​​da função são resolvidas é chamada de fechamento na literatura de ciência da computação - JavaScript, o guia definitivo de David Flanagan

A maioria das funções é chamada usando a mesma cadeia de escopo que estava em vigor quando a função foi definida, e não importa realmente se há um fechamento envolvido. Os fechamentos tornam-se interessantes quando são chamados sob uma cadeia de escopo diferente daquela que estava em vigor quando foram definidos. Isso acontece mais comumente quando um objeto de função aninhada é retornado da função na qual ele foi definido.

Quando a função retorna, esse objeto de ativação é removido da cadeia de escopo. Se não houver funções aninhadas, não haverá mais referências ao objeto de ativação e ele será coletado como lixo. Se houvesse funções aninhadas definidas, cada uma dessas funções terá uma referência à cadeia de escopo, e essa cadeia de escopo se refere ao objeto de ativação.

Se esses objetos de funções aninhadas permanecerem dentro de sua função externa, eles mesmos serão coletados como lixo, juntamente com o objeto de ativação a que se referiram. Mas se a função define uma função aninhada e a retorna ou a armazena em uma propriedade em algum lugar, haverá uma referência externa à função aninhada. Ele não será coletado como lixo e o objeto de ativação a que se refere também não será coletado.

No nosso exemplo, acima, não voltar f2a partir de f1, por isso, quando uma chamada de f1retorno, o seu objecto de activação será removido a partir do seu âmbito e cadeia de lixo recolhido. Mas se tivéssemos algo assim:

function f1(x) {

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

    return f2;
}

Aqui, o retorno f2terá uma cadeia de escopo que conterá o objeto de ativação de f1e, portanto, não será coletado como lixo. Nesse ponto, se chamarmos f2, ele poderá acessar f1a variável de xmesmo que estejamos sem f1.

Portanto, podemos ver que uma função mantém sua cadeia de escopo com ela e com a cadeia de escopo vêm todos os objetos de ativação de funções externas. Essa é a essência do fechamento. Dizemos que as funções no JavaScript têm "escopo lexical " , o que significa que elas salvam o escopo que estava ativo quando elas foram definidas, em oposição ao escopo que estava ativo quando foram chamadas.

Existem várias técnicas de programação poderosas que envolvem fechamentos, como aproximar variáveis ​​privadas, programação orientada a eventos, aplicação parcial etc.

Observe também que tudo isso se aplica a todos os idiomas que suportam fechamentos. Por exemplo, PHP (5.3+), Python, Ruby, etc.

codificador de árvore
fonte
-1

Um fechamento é uma otimização de compilador (também conhecido como açúcar sintático?). Algumas pessoas também se referiram a isso como o objeto do pobre homem .

Veja a resposta de Eric Lippert : (trecho abaixo)

O compilador irá gerar 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;
}

Faz sentido?
Além disso, você pediu comparações. O VB e o JScript criam fechamentos praticamente da mesma maneira.

LamonteCristo
fonte
Esta resposta é uma CW porque não mereço pontos pela grande resposta de Eric. Faça o voto positivo como achar melhor. HTH
goodguys_activate
3
-1: sua explicação é muito raiz em C #. O fechamento é usado em muitas línguas e é muito mais que o açúcar sintático nessas línguas e abrange tanto a função quanto o estado.
Martin York
1
Não, um fechamento não é apenas uma "otimização de compilador" nem açúcar sintático. -1