Qu'est-ce qu'une fermeture?


155

De temps en temps, je vois des "fermetures" mentionnées, et j'ai essayé de les rechercher, mais Wiki ne donne pas d'explication que je comprends. Quelqu'un pourrait-il m'aider ici?


Si vous connaissez Java / C #, espérez que ce lien vous aidera
Gulshan le

1
Les fermetures sont difficiles à comprendre. Vous devriez essayer de cliquer sur tous les liens dans la première phrase de cet article de Wikipedia et de comprendre ces articles en premier.
Zach


3
Quelle est la différence fondamentale entre une fermeture et une classe? Ok, une classe avec une seule méthode publique.
biziclop

5
@biziclop: Vous pouvez émuler une fermeture avec une classe (c'est ce que les développeurs Java doivent faire). Mais ils sont généralement un peu moins verbeux à créer et vous n'avez pas à gérer manuellement ce que vous transportez. (Les inconditionnels lispers posent une question similaire, mais parviennent probablement à cette autre conclusion: le support OO au niveau de la langue n’est pas nécessaire en cas de fermeture).

Réponses:


141

(Avertissement: ceci est une explication de base; en ce qui concerne la définition, je simplifie un peu)

Le moyen le plus simple de penser à une fermeture est une fonction qui peut être stockée en tant que variable (appelée "fonction de première classe"), qui a une capacité spéciale d'accéder à d'autres variables locales à l'étendue dans laquelle elle a été créée.

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

Les fonctions 1 assignées à document.onclicket displayValOfBlacksont des fermetures. Vous pouvez voir qu'ils font tous les deux référence à la variable booléenne black, mais cette variable est affectée en dehors de la fonction. Comme il blackest local à la portée où la fonction a été définie , le pointeur sur cette variable est préservé.

Si vous mettez ceci dans une page HTML:

  1. Cliquez pour passer au noir
  2. Appuyez sur [entrée] pour voir "true"
  3. Cliquez à nouveau pour revenir au blanc
  4. Appuyez sur [entrée] pour voir "faux"

Cela montre que les deux ont le même accès blacket peuvent être utilisés pour stocker l’état sans objet encapsuleur.

L'appel à setKeyPressest pour montrer comment une fonction peut être passée comme n'importe quelle variable. La portée conservée dans la fermeture est toujours celle où la fonction a été définie.

Les fermetures sont couramment utilisées comme gestionnaires d'événements, notamment dans JavaScript et ActionScript. Une bonne utilisation des fermetures vous aidera à lier implicitement des variables aux gestionnaires d'événements sans avoir à créer un wrapper d'objet. Cependant, une utilisation négligente entraînera des fuites de mémoire (par exemple, lorsqu'un gestionnaire d'événements inutilisé mais préservé est la seule chose à conserver pour les objets volumineux en mémoire, notamment les objets DOM, empêchant ainsi le garbage collection).


1: En fait, toutes les fonctions en JavaScript sont des fermetures.


3
En lisant votre réponse, j'ai senti une ampoule s'allumer dans mon esprit. Très appréciée! :)
Jay

1
Puisqu'il blackest déclaré dans une fonction, cela ne serait-il pas détruit lorsque la pile se déroulera ...?
Gablin

1
@ gablin, c'est ce qui est unique avec les langues fermées. Toutes les langues avec ramasse-miettes fonctionnent à peu près de la même manière: quand un objet ne contient plus de références, il peut être détruit. Chaque fois qu'une fonction est créée dans JS, la portée locale est liée à cette fonction jusqu'à ce que cette fonction soit détruite.
Nicole

2
@ gablin, c'est une bonne question. Je ne pense pas qu'ils ne peuvent pas & mdash; mais j’ai seulement évoqué le ramasse-miettes puisque c’est ce que JS utilise et c’est à cela que vous sembliez vous référer lorsque vous avez dit: "Puisque blackest déclaré dans une fonction, cela ne serait-il pas détruit". Rappelez-vous également que si vous déclarez un objet dans une fonction, puis l'affectez à une variable qui se trouve ailleurs, cet objet est préservé car il contient d'autres références.
Nicole

1
Objective-C (et C sous clang) supporte les blocs, qui sont essentiellement des fermetures, sans récupération de place. Cela nécessite un support d'exécution et une intervention manuelle autour de la gestion de la mémoire.
quixoto

68

Une fermeture est fondamentalement juste une façon différente de regarder un objet. Un objet est une donnée à laquelle une ou plusieurs fonctions sont liées. Une fermeture est une fonction à laquelle une ou plusieurs variables sont liées. Les deux sont fondamentalement identiques, au moins au niveau de la mise en œuvre. La vraie différence réside dans leur origine.

En programmation orientée objet, vous déclarez une classe d'objet en définissant ses variables de membre et ses méthodes (fonctions de membre) en amont, puis vous créez des instances de cette classe. Chaque instance est accompagnée d'une copie des données du membre, initialisée par le constructeur. Vous avez alors une variable de type d'objet que vous transmettez sous forme de donnée, car l'accent est mis sur sa nature en tant que donnée.

En revanche, dans une fermeture, l'objet n'est pas défini au préalable comme une classe d'objet, ni instancié via un appel de constructeur dans votre code. Au lieu de cela, vous écrivez la fermeture en tant que fonction dans une autre fonction. La fermeture peut faire référence à l'une des variables locales de la fonction externe. Le compilateur le détecte et déplace ces variables de l'espace de pile de la fonction externe vers la déclaration d'objet masqué de la fermeture. Vous avez alors une variable de type fermeture, et même s'il s'agit essentiellement d'un objet situé sous le capot, vous la transmettez en tant que référence de fonction, car l'accent est mis sur sa nature en tant que fonction.


3
+1: bonne réponse. Vous pouvez voir une fermeture en tant qu'objet avec une seule méthode et un objet arbitraire en tant que collection de fermetures sur des données sous-jacentes communes (les variables membres de l'objet). Je pense que ces deux points de vue sont assez symétriques.
Giorgio

3
Très bonne réponse. Cela explique en réalité l’idée de la fermeture.
RoboAlex

1
@Mason Wheeler: Où les données de fermeture sont-elles stockées? En pile comme une fonction? Ou en tas comme un objet?
RoboAlex

1
@RoboAlex: dans le tas, parce que c'est un objet qui ressemble à une fonction.
Mason Wheeler

1
@RoboAlex: l'emplacement de la fermeture et de ses données capturées dépend de l'implémentation. En C ++, il peut être stocké dans le tas ou sur la pile.
Giorgio

29

Le terme de fermeture vient du fait qu'un morceau de code (bloc, fonction) peut avoir des variables libres qui sont fermées (c'est-à-dire liées à une valeur) par l'environnement dans lequel le bloc de code est défini.

Prenons par exemple la définition de la fonction Scala:

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

Dans le corps de la fonction , il existe deux noms (variables) vet kindiquant deux valeurs entières. Le nom vest lié car il est déclaré en tant qu'argument de la fonction addConstant(en regardant la déclaration de fonction, nous savons qu'une valeur vsera attribuée lorsque la fonction est appelée). Le nom kest libre par rapport à la fonction addConstantcar celle-ci ne contient aucune indication sur la valeur kliée (et comment).

Afin d'évaluer un appel comme:

val n = addConstant(10)

nous devons assigner kune valeur, ce qui ne peut se produire que si le nom kest défini dans le contexte dans lequel addConstantest défini. Par exemple:

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

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

  values.map(addConstant)
}

Maintenant que nous avons défini addConstantdans un contexte où kest défini, addConstantest devenu une clôture car toutes ses variables libres sont maintenant fermées (liées à une valeur): addConstantpeuvent être invoquées et transmises comme s'il s'agissait d'une fonction. Notez que la variable libre kest liée à une valeur lorsque la fermeture est définie , alors que la variable d'argument vest liée lorsque la fermeture est invoquée .

Ainsi, une fermeture est fondamentalement une fonction ou un bloc de code qui peut accéder à des valeurs non locales via ses variables libres après que celles-ci ont été liées par le contexte.

Dans de nombreuses langues, si vous utilisez une fermeture une seule fois, vous pouvez la rendre anonyme , par exemple:

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

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

Notez qu'une fonction sans variables libres est un cas particulier de fermeture (avec un ensemble vide de variables libres). De manière analogue, une fonction anonyme est un cas particulier de fermeture anonyme. En d'autres termes , une fonction anonyme est une fermeture anonyme sans variables libres.


Cela s'accorde bien avec les formules fermées et ouvertes en logique. Merci pour votre réponse.
RainDoctor

@RainDoctor: Les variables libres sont définies de la même manière dans les formules logiques et dans les expressions de lambda calcul: le lambda dans une expression lambda fonctionne comme un quantificateur dans les formules logiques à l'aide de variables libres / liées.
Giorgio

9

Une explication 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)utilisera la valeur précédemment créée de closure. L' alertValueespace de noms de la fonction retournée sera connecté à l'espace de noms dans lequel closureréside la variable. Lorsque vous supprimez l'intégralité de la fonction, la valeur de la closurevariable sera supprimée, mais jusqu'à cette date, la alertValuefonction sera toujours en mesure de lire / écrire la valeur de la variable closure.

Si vous exécutez ce code, la première itération attribue la valeur 0 à la closurevariable et réécrit la fonction pour:

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

Et comme il a alertValuebesoin de la variable locale closurepour exécuter la fonction, il se lie à la valeur de la variable locale précédemment assignée closure.

Et maintenant, chaque fois que vous appelez la closure_examplefonction, elle écrira la valeur incrémentée de la closurevariable car alert(closure)est liée.

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

merci, je n'ai pas testé le code =) tout semble bien se passer maintenant.
Muha le

5

Une "fermeture" est, en substance, un état local et du code, combinés dans un paquet. En général, l'état local provient d'une portée (lexicale) environnante et le code est (essentiellement) une fonction interne qui est ensuite renvoyée à l'extérieur. La fermeture est alors une combinaison des variables capturées vues par la fonction interne et du code de la fonction interne.

C'est une de ces choses qui est, malheureusement, un peu difficile à expliquer, à cause de son ignorance.

Une analogie que j'ai utilisée avec succès dans le passé était «imaginons que nous ayons quelque chose que nous appelons« le livre », dans la fermeture de la salle,« le livre »est cette copie là-bas, dans le coin, de TAOCP, mais sur la table de fermeture. , c’est cette copie d’un livre de Dresden Files. Ainsi, en fonction de la fermeture dans laquelle vous vous trouvez, le code "donnez-moi le livre" a pour résultat que différentes choses se passent. "


Vous avez oublié ceci: en.wikipedia.org/wiki/Closure_(computer_programming) dans votre réponse.
S.Lott

3
Non, j'ai consciemment choisi de ne pas fermer cette page.
Vatine

"Etat et fonction": une fonction C avec une staticvariable locale peut-elle être considérée comme une fermeture? Les fermetures à Haskell impliquent-elles un état?
Giorgio

2
@Giorgio Les fermetures à Haskell rapprochent (je crois) des arguments dans la portée lexicale dans laquelle ils sont définis, alors je dirais "oui" (bien que je sois au mieux inconnu de Haskell). La fonction AC avec une variable statique est, au mieux, une fermeture très limitée (vous voulez vraiment pouvoir créer plusieurs fermetures à partir d'une seule fonction, avec une staticvariable locale, vous en avez exactement une).
Vatine

J'ai volontairement posé cette question parce que je pense qu'une fonction C avec une variable statique n'est pas une fermeture: la variable statique est définie localement et connue uniquement à l'intérieur de la fermeture, elle n'a pas accès à l'environnement. De plus, je ne suis pas sûr à 100%, mais je formulerais votre déclaration dans l'autre sens: vous utilisez le mécanisme de fermeture pour créer différentes fonctions (une fonction est une définition de fermeture + une liaison pour ses variables libres).
Giorgio

5

Il est difficile de définir ce qu'est la clôture sans définir le concept d '«État».

Fondamentalement, dans un langage avec une portée lexicale complète qui traite les fonctions comme des valeurs de première classe, quelque chose de spécial se produit. Si je devais faire quelque chose comme:

function foo(x)
return x
end

x = foo

La variable xnon seulement référence function foo()mais aussi référence à l’état qui a fooété laissé lors du dernier retour. La vraie magie se produit quand food'autres fonctions sont définies plus en détail dans son champ d'application; c'est comme son propre mini-environnement (tout comme 'normalement' nous définissons des fonctions dans un environnement global).

Sur le plan fonctionnel, il peut résoudre bon nombre des mêmes problèmes que le mot clé 'statique' de C ++ (C?), Qui conserve l'état d'une variable locale tout au long de plusieurs appels de fonction; Cependant, cela ressemble plus à l'application du même principe (variable statique) à une fonction, car les fonctions sont des valeurs de première classe; fermeture ajoute la prise en charge de l’ensemble de l’état de la fonction à enregistrer (rien à voir avec les fonctions statiques de C ++).

Traiter les fonctions comme des valeurs de première classe et ajouter la prise en charge des fermetures signifie également que vous pouvez avoir plus d'une instance de la même fonction en mémoire (similaire aux classes). Cela signifie que vous pouvez réutiliser le même code sans avoir à réinitialiser l'état de la fonction, comme cela est nécessaire pour traiter les variables statiques C ++ dans une fonction (cela peut être faux à ce sujet?).

Voici quelques tests du support de fermeture 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

résultats:

nil
20
31
42

Cela peut devenir compliqué, et cela varie probablement d’une langue à l’autre, mais il semble en Lua que chaque fois qu’une fonction est exécutée, son état est réinitialisé. Je dis cela parce que les résultats du code ci-dessus seraient différents si nous accédions myclosuredirectement à la fonction / à l’état (au lieu de passer par la fonction anonyme pvaluerenvoyée ), ce qui reviendrait à 10; mais si nous accédons à l'état de myclosure par x (la fonction anonyme), vous pouvez voir qu'il pvalueest bien vivant quelque part dans la mémoire. J'imagine qu'il y a un peu plus, mais quelqu'un peut peut-être mieux expliquer la nature de la mise en œuvre.

PS: Je ne connais pas une couche de C ++ 11 (autre que celles des versions précédentes), alors notez qu'il ne s'agit pas d'une comparaison entre les fermetures en C ++ 11 et Lua. En outre, toutes les "lignes" tracées de Lua à C ++ sont des similitudes en tant que variables statiques et fermetures ne sont pas identiques à 100%; même s’ils sont parfois utilisés pour résoudre des problèmes similaires.

Ce dont je ne suis pas sûr, c’est, dans l’exemple de code ci-dessus, si la fonction anonyme ou la fonction d’ordre supérieur est considérée comme la fermeture?


4

Une fermeture est une fonction à laquelle l'état associé:

En perl, vous créez des fermetures comme celle-ci:

#!/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 nous regardons la nouvelle fonctionnalité fournie avec C ++.
Il vous permet également de lier l’état actuel à l’objet:

#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

Considérons une fonction simple:

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

Cette fonction s'appelle une fonction de niveau supérieur car elle n'est imbriquée dans aucune autre fonction. Chaque fonction JavaScript s’associe à une liste d’objets appelée "chaîne d’étendue" . Cette chaîne d'étendue est une liste ordonnée d'objets. Chacun de ces objets définit des variables.

Dans les fonctions de niveau supérieur, la chaîne d'étendue est constituée d'un seul objet, l'objet global. Par exemple, la fonction f1ci-dessus a une chaîne d'étendue qui contient un seul objet qui définit toutes les variables globales. (Notez que le terme "objet" ne signifie pas ici d'objet JavaScript, il s'agit simplement d'un objet défini par l'implémentation qui agit comme un conteneur de variables, dans lequel JavaScript peut "rechercher" des variables.)

Lorsque cette fonction est appelée, JavaScript crée un objet appelé "objet d'activation" et le place en haut de la chaîne d'étendue. Cet objet contient toutes les variables locales (par exemple xici). Nous avons donc maintenant deux objets dans la chaîne d’étendue: le premier est l’objet d’activation et, au-dessous, l’objet global.

Notez très attentivement que les deux objets sont placés dans la chaîne de l'oscilloscope à des moments différents. L'objet global est placé lorsque la fonction est définie (c'est-à-dire lorsque JavaScript a été analysé et que l'objet fonction a été créé) et l'objet d'activation est entré lorsque la fonction a été appelée.

Donc, nous savons maintenant ceci:

  • Chaque fonction est associée à une chaîne d'étendue
  • Lorsque la fonction est définie (lorsque l'objet de fonction est créé), JavaScript enregistre une chaîne d'étendue avec cette fonction.
  • Pour les fonctions de niveau supérieur, la chaîne d'étendue contient uniquement l'objet global au moment de la définition de la fonction et ajoute un objet d'activation supplémentaire en haut au moment de l'appel.

La situation devient intéressante lorsque nous traitons avec des fonctions imbriquées. Alors, créons-en un:

function f1(x) {

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

}

Une fois f1défini, nous obtenons une chaîne de portée contenant uniquement l'objet global.

Désormais, lorsqu’elle f1est appelée, la chaîne d’étendue de f1obtient l’objet d’activation. Cet objet d'activation contient la variable xet la variable f2qui est une fonction. Et, notez que cela f2est en train de se définir. Par conséquent, à ce stade, JavaScript enregistre également une nouvelle chaîne d'étendue pour f2. La chaîne d'étendue enregistrée pour cette fonction interne est la chaîne d'étendue actuelle en vigueur. La chaîne de champs d'application en vigueur est celle de f1's. Par conséquent f2« la chaîne de portée est f1» de courant chaîne de portée - qui contient l'objet d'activation f1et de l'objet global.

Quand f2est appelé, il obtient son propre objet d'activation contenant y, ajouté à sa chaîne d'étendue qui contient déjà l'objet d'activation de f1et l'objet global.

Si une autre fonction imbriquée était définie au sein de celle-ci f2, sa chaîne d'étendue contiendrait trois objets au moment de la définition (2 objets d'activation de deux fonctions externes et l'objet global) et 4 au moment de l'appel.

Nous comprenons donc maintenant comment fonctionne la chaîne d’étendue, mais nous n’avons pas encore parlé de fermeture.

La combinaison d'un objet de fonction et d'une portée (un ensemble de liaisons de variables) dans laquelle les variables de la fonction sont résolues est appelée une fermeture dans la littérature informatique - JavaScript le guide définitif de David Flanagan

La plupart des fonctions sont appelées à l'aide de la même chaîne d'étendue que celle qui était en vigueur lors de la définition de la fonction, et peu importe qu'il y ait une fermeture. Les fermetures deviennent intéressantes quand elles sont appelées dans une chaîne de champs différente de celle qui était en vigueur lors de leur définition. Cela se produit le plus souvent lorsqu'un objet de fonction imbriqué est renvoyé à partir de la fonction dans laquelle il a été défini.

Lorsque la fonction revient, cet objet d'activation est supprimé de la chaîne d'étendue. S'il n'y a pas de fonctions imbriquées, il n'y a plus de référence à l'objet d'activation et il est récupéré. Si des fonctions imbriquées ont été définies, chacune de ces fonctions a une référence à la chaîne d'étendue et cette chaîne d'étendue fait référence à l'objet d'activation.

Si ces fonctions imbriquées restent dans leur fonction externe, elles seront elles-mêmes collectées, ainsi que l'objet d'activation auquel elles font référence. Mais si la fonction définit une fonction imbriquée et la renvoie ou la stocke dans une propriété quelque part, il y aura une référence externe à la fonction imbriquée. Il ne sera pas nettoyé et l'objet d'activation auquel il fait référence ne le sera pas non plus.

Dans notre exemple ci-dessus, nous ne retournons pas f2depuis f1. Par conséquent, lorsqu'un appel f1retourne, son objet d'activation est supprimé de la chaîne d'étendue et les ordures collectées. Mais si nous avions quelque chose comme ça:

function f1(x) {

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

    return f2;
}

Ici, le retour f2aura une chaîne d'étendue qui contiendra l'objet d'activation de f1, et par conséquent, il ne sera pas récupéré. À ce stade, si nous appelons f2, il sera en mesure d'accéder à f1la variable, xmême si nous sommes en dehors de f1.

Nous pouvons donc voir qu’une fonction garde sa chaîne d’étendue avec elle et qu’elle contient tous les objets d’activation des fonctions externes. C'est l'essence de la fermeture. Nous disons que les fonctions en JavaScript ont une "portée lexicale" , ce qui signifie qu'elles sauvegardent la portée qui était active quand elles ont été définies par opposition à la portée qui était active quand elles ont été appelées.

Il existe un certain nombre de techniques de programmation puissantes qui impliquent des fermetures telles que l'approximation de variables privées, la programmation événementielle, l'application partielle , etc.

Notez également que tout cela s'applique à toutes les langues prenant en charge les fermetures. Par exemple, PHP (5.3+), Python, Ruby, etc.


-1

Une fermeture est une optimisation du compilateur (aka sucre syntaxique?). Certaines personnes ont également qualifié cet objet d'objet du pauvre .

Voir la réponse d'Eric Lippert : (extrait ci-dessous)

Le compilateur générera le code comme ceci:

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

Avoir un sens?
En outre, vous avez demandé des comparaisons. VB et JScript créent tous deux des fermetures de la même manière.


Cette réponse est une CW car je ne mérite pas de points pour la réponse géniale d'Eric. S'il vous plaît upvote comme bon vous semble. HTH
goodguys_activate

3
-1: Votre explication est trop root en C #. La fermeture est utilisée dans de nombreuses langues et est beaucoup plus que du sucre syntaxique dans ces langues et englobe à la fois la fonction et l'état.
Martin York le

1
Non, une fermeture n'est pas simplement une "optimisation du compilateur", ni un sucre syntaxique. -1
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.