Que sont les «fermetures» dans .NET?

195

Qu'est-ce qu'une fermeture ? Les avons-nous en .NET?

S'ils existent dans .NET, pourriez-vous s'il vous plaît fournir un extrait de code (de préférence en C #) l'expliquant?

Développeur
la source

Réponses:

258

J'ai un article sur ce même sujet . (Il contient de nombreux exemples.)

En substance, une fermeture est un bloc de code qui peut être exécuté ultérieurement, mais qui maintient l'environnement dans lequel il a été créé pour la première fois - c'est-à-dire qu'il peut toujours utiliser les variables locales, etc. de la méthode qui l'a créé, même après cela. La méthode a terminé son exécution.

La fonctionnalité générale des fermetures est implémentée en C # par des méthodes anonymes et des expressions lambda.

Voici un exemple utilisant une méthode anonyme:

using System;

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

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

Production:

counter=1
counter=2

Ici, nous pouvons voir que l'action retournée par CreateAction a toujours accès à la variable compteur, et peut en effet l'incrémenter, même si CreateAction elle-même est terminée.

Jon Skeet
la source
57
Merci Jon. BTW, y a-t-il quelque chose que vous ne connaissez pas dans .NET? :) À qui allez-vous lorsque vous avez des questions?
Développeur
44
Il y a toujours plus à apprendre :) Je viens de terminer la lecture de CLR via C # - très instructif. En dehors de cela, je demande généralement à Marc Gravell pour les arbres WCF / binding / expression et à Eric Lippert pour les choses en langage C #.
Jon Skeet
2
Je l'ai remarqué, mais je pense toujours que votre déclaration selon laquelle il s'agit d'un "bloc de code qui peut être exécuté ultérieurement" est tout simplement incorrecte - cela n'a rien à voir avec l'exécution, plus avec les valeurs des variables et la portée qu'avec l'exécution. , en soi.
Jason Bunting
11
Je dirais que les fermetures ne sont utiles que si elles peuvent être exécutées, et "à une date ultérieure" met en évidence la "bizarrerie" de pouvoir capturer l'environnement (qui autrement aurait pu disparaître au moment de l'exécution). Si vous ne citez que la moitié de la phrase, c'est bien sûr une réponse incomplète.
Jon Skeet
4
@SLC: Oui, counterest disponible pour être incrémenté - le compilateur génère une classe qui contient un counterchamp, et tout code faisant référence counterfinit par passer par une instance de cette classe.
Jon Skeet
22

Si vous souhaitez voir comment C # implémente Closure, lisez "Je connais la réponse (son 42) blog"

Le compilateur génère une classe en arrière-plan pour encapsuler la méthode anoyme et la variable j

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

pour la fonction:

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

Le transformer en:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}
Daniil Grankin
la source
Salut, Daniil - Votre réponse est très utile et je voulais aller au-delà de votre réponse et faire un suivi, mais le lien est rompu. Malheureusement, mon googlefu n'est pas assez bon pour trouver où il s'est déplacé.
Knox
10

Les fermetures sont des valeurs fonctionnelles qui conservent des valeurs variables de leur portée d'origine. C # peut les utiliser sous forme de délégués anonymes.

Pour un exemple très simple, prenez ce code C #:

    delegate int testDel();

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

    }

À la fin de celui-ci, la barre sera définie sur 4 et le délégué myClosure peut être transmis pour être utilisé ailleurs dans le programme.

Les fermetures peuvent être utilisées pour de nombreuses choses utiles, comme l'exécution retardée ou pour simplifier les interfaces - LINQ est principalement construit à l'aide de fermetures. Pour la plupart des développeurs, la manière la plus immédiate qui soit utile consiste à ajouter des gestionnaires d'événements aux contrôles créés dynamiquement - vous pouvez utiliser des fermetures pour ajouter un comportement lorsque le contrôle est instancié, plutôt que de stocker des données ailleurs.

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

Une fermeture est une fonction anonyme passée en dehors de la fonction dans laquelle elle est créée. Il conserve toutes les variables de la fonction dans laquelle il est créé qu'il utilise.

AnthonyWJones
la source
4

Voici un exemple artificiel pour C # que j'ai créé à partir d'un code similaire en JavaScript:

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

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

Voici donc du code qui montre comment utiliser le code ci-dessus ...

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

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

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

J'espère que cela est quelque peu utile.

Jason Bunting
la source
1
Vous avez donné un exemple, mais vous n'avez pas proposé de définition générale. Je comprends de vos commentaires ici qu'ils sont «plus sur la portée», mais il y a sûrement plus que cela?
ladenedge
2

Fondamentalement, la fermeture est un bloc de code que vous pouvez passer comme argument à une fonction. C # prend en charge les fermetures sous forme de délégués anonymes.

Voici un exemple simple:
La méthode List.Find peut accepter et exécuter un morceau de code (fermeture) pour rechercher l'élément de la liste.

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

En utilisant la syntaxe C # 3.0, nous pouvons écrire ceci comme:

ints.Find(value => value == 1);
aku
la source
1
Je déteste être technique, mais la fermeture a plus à voir avec la portée - une fermeture peut être créée de deux manières différentes, mais une fermeture n'est pas le moyen, c'est la fin.
Jason Bunting
2

Une fermeture se produit lorsqu'une fonction est définie à l'intérieur d'une autre fonction (ou méthode) et qu'elle utilise les variables de la méthode parente . Cette utilisation de variables qui sont situées dans une méthode et enveloppées dans une fonction définie en son sein, est appelée une fermeture.

Mark Seemann a quelques exemples intéressants de fermetures dans son article de blog où il fait un parallèle entre la programmation oop et la programmation fonctionnelle.

Et pour le rendre plus détaillé

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

Les fermetures sont des morceaux de code qui font référence à une variable en dehors d'eux-mêmes, (en dessous d'eux sur la pile), qui pourraient être appelés ou exécutés plus tard, (comme lorsqu'un événement ou un délégué est défini, et pourraient être appelés à un moment futur indéfini ) Parce que la variable externe que le bloc de références de code peut être hors de portée (et aurait sinon été perdue), le fait qu'elle soit référencée par le bloc de code (appelé une fermeture) indique au runtime de "maintenir "cette variable dans la portée jusqu'à ce qu'elle ne soit plus nécessaire par le morceau de fermeture du code ...

Charles Bretana
la source
Comme je l'ai indiqué dans l'explication de quelqu'un d'autre: je déteste être technique, mais la fermeture a plus à voir avec la portée - une fermeture peut être créée de deux manières différentes, mais une fermeture n'est pas le moyen, c'est la fin.
Jason Bunting
1
Les fermetures sont relativement nouvelles pour moi, il est donc tout à fait possible que je comprenne mal, mais j'obtiens la partie portée. Ma réponse est centrée sur la portée. Donc, je manque ce que votre commentaire essaie de corriger ... À quoi d'autre la portée peut-elle être pertinente, mais un morceau de code? (fonction, méthode anonyme ou autre)
Charles Bretana
N'est-ce pas la clé d'une fermeture selon laquelle un "morceau de code exécutable" peut accéder à une variable ou à une valeur en mémoire qui est syntaxiquement "en dehors" de sa portée, après que cette variable aurait normalement dû être "hors de portée" ou avoir été détruite ?
Charles Bretana
Et @Jason, pas de soucis d'être technique, cette idée de fermeture est quelque chose que j'ai pris un certain temps pour envelopper ma tête, dans de longues discussions avec un collègue, sur les fermetures javascript ... mais il était un écrou Lisp et je n'ai jamais a bien traversé les abstractions dans ses explications ...
Charles Bretana
0

J'ai essayé de le comprendre aussi, bien en dessous se trouvent les extraits de code pour le même code en Javascript et C # montrant la fermeture.

  1. Compter le nombre de fois où chaque événement s'est produit ou le nombre de fois où chaque bouton a été cliqué.

JavaScript:

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

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

  return inner;
};

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

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

C #:

using System.IO;
using System;

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

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

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

public class a {

    int b = 0;

    public  void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
  1. compter le nombre total de fois où un événement de clic s'est produit ou compter le nombre total de clics, quel que soit le contrôle.

JavaScript:

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

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

  return inner;
};

var a = c();

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

C #:

using System.IO;
using System;

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

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

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

public class a {

    static int b = 0;

    public void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
Vimalshah26
la source
0

Juste à l'improviste, une réponse simple et plus compréhensive du livre C # 7.0 en bref.

Pré-requis que vous devez savoir : Une expression lambda peut référencer les variables locales et les paramètres de la méthode dans laquelle elle est définie (variables externes).

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

Partie réelle : les variables externes référencées par une expression lambda sont appelées variables capturées. Une expression lambda qui capture des variables est appelée fermeture.

Dernier point à noter : les variables capturées sont évaluées lorsque le délégué est effectivement appelé, et non lorsque les variables ont été capturées:

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

Si vous écrivez une méthode anonyme en ligne (C # 2) ou (de préférence) une expression Lambda (C # 3 +), une méthode réelle est toujours en cours de création. Si ce code utilise une variable locale de portée externe - vous devez toujours transmettre cette variable à la méthode d'une manière ou d'une autre.

par exemple, prenez cette clause Linq Where (qui est une méthode d'extension simple qui transmet une expression lambda):

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

si vous voulez utiliser i dans cette expression lambda, vous devez le passer à cette méthode créée.

La première question qui se pose est donc: doit-elle être transmise par valeur ou référence?

Le passage par référence est (je suppose) plus préférable car vous obtenez un accès en lecture / écriture à cette variable (et c'est ce que fait C #; je suppose que l'équipe de Microsoft a pesé le pour et le contre et a opté pour une référence par référence; selon Jon Skeet's article , Java est allé avec par valeur).

Mais alors une autre question se pose: où allouer ce i?

Doit-il réellement / naturellement être alloué sur la pile? Eh bien, si vous l'allouez sur la pile et la transmettez par référence, il peut y avoir des situations où elle survit à son propre cadre de pile. Prenez cet exemple:

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

static IEnumerable<string> whereItems;

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

L'expression lambda (dans la clause Where) crée à nouveau une méthode qui fait référence à un i. Si i est alloué sur la pile d'Outlive, au moment où vous énumérez les whereItems, le i utilisé dans la méthode générée pointera vers le i d'Outlive, c'est-à-dire vers un endroit de la pile qui n'est plus accessible.

Ok, nous en avons donc besoin sur le tas alors.

Donc, ce que le compilateur C # fait pour prendre en charge cette ligne anonyme / lambda, c'est utiliser ce qu'on appelle des " fermetures ": il crée une classe sur le tas appelée ( plutôt mal ) DisplayClass qui a un champ contenant le i, et la fonction qui utilise réellement il.

Quelque chose qui serait équivalent à cela (vous pouvez voir l'IL généré en utilisant ILSpy ou ILDASM):

class <>c_DisplayClass1
{
    public int i;

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

Il instancie cette classe dans votre portée locale et remplace tout code relatif à i ou à l'expression lambda par cette instance de fermeture. Donc - chaque fois que vous utilisez le i dans votre code "portée locale" où j'ai été défini, vous utilisez en fait ce champ d'instance DisplayClass.

Donc, si je modifiais le "local" i dans la méthode principale, cela changerait en fait _DisplayClass.i;

c'est à dire

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

il affichera 12, car "i = 10" va dans ce champ dispalyclass et le modifie juste avant la 2ème énumération.

Une bonne source sur le sujet est ce module Bart De Smet Pluralsight (nécessite une inscription) (ignorez également son utilisation erronée du terme "Levage" - ce qu'il (je pense) veut dire, c'est que la variable locale (c'est-à-dire i) est modifiée pour se référer dans le nouveau champ DisplayClass).


Dans d'autres nouvelles, il semble y avoir une idée fausse selon laquelle les "fermetures" sont liées aux boucles - si je comprends bien, les "fermetures" ne sont PAS un concept lié aux boucles , mais plutôt aux méthodes anonymes / expressions lambda utilisant des variables de portée locales - bien que certaines astuces les questions utilisent des boucles pour le démontrer.

David Refaeli
la source
-1

Une fermeture est une fonction, définie au sein d'une fonction, qui peut accéder à ses variables locales ainsi qu'à son parent.

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

donc la fonction à l'intérieur de la méthode find.

 t => t.Name == name

peut accéder aux variables dans sa portée, t, et le nom de la variable qui est dans sa portée parent. Même s'il est exécuté par la méthode find en tant que délégué, à partir d'une autre étendue tous ensemble.

DevelopingChris
la source
2
Une fermeture n'est pas une fonction en soi, elle est plus définie en parlant de portée que de fonctions. Les fonctions aident simplement à maintenir la portée, ce qui entraîne la création d'une fermeture. Mais dire qu'une fermeture est une fonction n'est pas techniquement correct. Désolé de chiquer. :)
Jason Bunting