Was sind "Schließungen" in .NET?


195

Was ist eine Schließung ? Haben wir sie in .NET?

Wenn sie in .NET vorhanden sind, können Sie bitte ein Code-Snippet (vorzugsweise in C #) bereitstellen, das dies erklärt.

Antworten:


258

Ich habe einen Artikel zu diesem Thema . (Es gibt viele Beispiele.)

Im Wesentlichen ist ein Abschluss ein Codeblock, der zu einem späteren Zeitpunkt ausgeführt werden kann, aber die Umgebung beibehält, in der er zuerst erstellt wurde - dh er kann auch danach noch die lokalen Variablen usw. der Methode verwenden, die ihn erstellt hat Die Ausführung der Methode ist beendet.

Die allgemeine Funktion von Closures wird in C # durch anonyme Methoden und Lambda-Ausdrücke implementiert.

Hier ist ein Beispiel mit einer anonymen Methode:

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

Ausgabe:

counter=1
counter=2

Hier können wir sehen, dass die von CreateAction zurückgegebene Aktion weiterhin Zugriff auf die Zählervariable hat und diese tatsächlich erhöhen kann, obwohl CreateAction selbst abgeschlossen ist.


57
Danke Jon. Übrigens, gibt es etwas, das Sie in .NET nicht kennen? :) Zu wem gehst du, wenn du Fragen hast?
Entwickler

44
Es gibt immer mehr zu lernen :) Ich habe gerade CLR über C # gelesen - sehr informativ. Abgesehen davon frage ich normalerweise Marc Gravell nach WCF- / Bindungs- / Expressionsbäumen und Eric Lippert nach C # -Sprachendingen.
Jon Skeet

2
Ich habe das bemerkt, aber ich denke immer noch, dass Ihre Aussage, dass es sich um einen "Codeblock handelt, der zu einem späteren Zeitpunkt ausgeführt werden kann", einfach falsch ist - es hat nichts mit der Ausführung zu tun, sondern eher mit variablen Werten und dem Umfang als mit der Ausführung per se.
Jason Bunting

11
Ich würde sagen, dass Schließungen nur dann nützlich sind, wenn sie ausgeführt werden können, und das "zu einem späteren Zeitpunkt" unterstreicht die "Seltsamkeit", die Umgebung erfassen zu können (die sonst möglicherweise durch die Ausführungszeit verschwunden wäre). Wenn Sie nur den halben Satz zitieren , ist dies natürlich eine unvollständige Antwort.
Jon Skeet

4
@SLC: Ja, counterkann inkrementiert werden. Der Compiler generiert eine Klasse, die ein counterFeld enthält , und jeder Code, auf den counterverwiesen wird, durchläuft eine Instanz dieser Klasse.
Jon Skeet

22

Wenn Sie sehen möchten, wie C # Closure implementiert, lesen Sie "Ich kenne die Antwort (sein 42) Blog".

Der Compiler generiert im Hintergrund eine Klasse, um die anoymöse Methode und die Variable j zu kapseln

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

für die Funktion:

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

Verwandeln Sie es in:

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

Hallo Daniil - Ihre Antwort ist sehr nützlich und ich wollte über Ihre Antwort hinausgehen und nachverfolgen, aber der Link ist defekt. Leider ist mein GoogleFU nicht gut genug, um herauszufinden, wohin es verschoben wurde.
Knox

10

Closures sind Funktionswerte, die variable Werte aus ihrem ursprünglichen Bereich beibehalten. C # kann sie in Form von anonymen Delegierten verwenden.

Nehmen Sie für ein sehr einfaches Beispiel diesen C # -Code:

    delegate int testDel();

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

    }

Am Ende wird die Leiste auf 4 gesetzt, und der myClosure-Delegat kann weitergegeben werden, um an anderer Stelle im Programm verwendet zu werden.

Closures können für viele nützliche Dinge verwendet werden, z. B. für die verzögerte Ausführung oder zur Vereinfachung von Schnittstellen. LINQ wird hauptsächlich mit Closures erstellt. Für die meisten Entwickler ist es am unmittelbarsten, Ereignisbehandlungsroutinen zu dynamisch erstellten Steuerelementen hinzuzufügen. Sie können Abschlüsse verwenden, um Verhalten hinzuzufügen, wenn das Steuerelement instanziiert wird, anstatt Daten an anderer Stelle zu speichern.


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

Ein Abschluss ist eine anonyme Funktion, die außerhalb der Funktion übergeben wird, in der er erstellt wurde. Es verwaltet alle Variablen aus der Funktion, in der es erstellt wird, die es verwendet.


4

Hier ist ein erfundenes Beispiel für C #, das ich aus ähnlichem Code in JavaScript erstellt habe:

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

Hier ist ein Code, der zeigt, wie der obige Code verwendet wird ...

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

Hoffe das ist etwas hilfreich.


1
Sie haben ein Beispiel gegeben, aber keine allgemeine Definition angeboten. Ich entnehme Ihren Kommentaren hier, dass es sich um mehr um den Umfang handelt, aber es steckt doch noch mehr dahinter?
beladen

2

Grundsätzlich ist Closure ein Codeblock, den Sie als Argument an eine Funktion übergeben können. C # unterstützt Schließungen in Form von anonymen Delegierten.

Hier ist ein einfaches Beispiel: Die
List.Find-Methode kann Code akzeptieren und ausführen (Abschluss), um das Element der Liste zu finden.

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

Mit der C # 3.0-Syntax können wir Folgendes schreiben:

ints.Find(value => value == 1);

1
Ich hasse es, technisch zu sein, aber die Schließung hat mehr mit dem Umfang zu tun - eine Schließung kann auf verschiedene Arten erstellt werden, aber eine Schließung ist nicht das Mittel, sondern das Ziel.
Jason Bunting

2

Ein Abschluss liegt vor, wenn eine Funktion in einer anderen Funktion (oder Methode) definiert ist und die Variablen der übergeordneten Methode verwendet . Diese Verwendung von Variablen, die sich in einer Methode befinden und in eine darin definierte Funktion eingeschlossen sind, wird als Abschluss bezeichnet.

Mark Seemann hat einige interessante Beispiele für Schließungen in seinem Blogbeitrag , wo er hat eine paralel zwischen oop und funktionalen Programmierung.

Und um es detaillierter zu machen

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.

1

Closures sind Codestücke, die auf eine Variable außerhalb von sich selbst verweisen (von unten auf dem Stapel), die möglicherweise später aufgerufen oder ausgeführt werden (z. B. wenn ein Ereignis oder ein Delegat definiert ist und zu einem unbestimmten zukünftigen Zeitpunkt aufgerufen werden kann) ) ... Da die externe Variable, auf die der Codeblock verweist, möglicherweise nicht mehr gültig ist (und sonst verloren gegangen wäre), weist die Tatsache, dass der Codeblock (als Closure bezeichnet) auf ihn verweist, die Laufzeit an, "zu halten" "Diese Variable im Gültigkeitsbereich, bis sie vom Abschlusscode-Code nicht mehr benötigt wird ...


Wie ich in der Erklärung eines anderen angedeutet habe: Ich hasse es, technisch zu sein, aber die Schließung hat mehr mit dem Umfang zu tun - eine Schließung kann auf verschiedene Arten erstellt werden, aber eine Schließung ist nicht das Mittel, sondern das Ziel.
Jason Bunting

1
Verschlüsse sind für mich relativ neu, daher ist es durchaus möglich, dass ich sie falsch verstehe, aber ich verstehe den Umfangsteil. Meine Antwort konzentriert sich auf den Umfang. Mir fehlt also, was Ihr Kommentar zu korrigieren versucht ... Was kann der Bereich außer für einen Teil des Codes noch relevant sein? (Funktion, anonyme Methode oder was auch immer)
Charles Bretana

Ist nicht der Schlüssel zu einem Abschluss, dass ein "Teil des ausführbaren Codes" auf eine Variable oder einen speicherinternen Wert zugreifen kann, der syntaktisch "außerhalb" seines Gültigkeitsbereichs liegt, nachdem diese Variable normalerweise "außerhalb des Gültigkeitsbereichs" oder zerstört worden sein sollte ?
Charles Bretana

Und @Jason, keine Sorge, technisch zu sein, diese Verschlussidee ist etwas, das ich eine Weile gebraucht habe, um mich in langen Diskussionen mit einem Kollegen über Javascript-Verschlüsse zu wickeln ... aber er war eine Lisp-Nuss und ich nie ganz durch die Abstraktionen in seinen Erklärungen gekommen ...
Charles Bretana

0

Ich habe versucht, es auch zu verstehen. Weiter unten finden Sie die Codefragmente für Same Code in Javascript und C #, die den Abschluss anzeigen.

  1. Zählen Sie, wie oft jedes Ereignis stattgefunden hat oder wie oft jede Schaltfläche angeklickt wurde.

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. Anzahl Gesamtzahl der Klickereignisse oder Anzahl der Klicks insgesamt, unabhängig von der Kontrolle.

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

0

Aus heiterem Himmel eine einfache und verständnisvollere Antwort aus dem Buch C # 7.0 auf den Punkt gebracht.

Voraussetzung, die Sie kennen sollten : Ein Lambda-Ausdruck kann auf die lokalen Variablen und Parameter der Methode verweisen, in der er definiert ist (äußere Variablen).

    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
    }

Realteil : Äußere Variablen, auf die ein Lambda-Ausdruck verweist, werden als erfasste Variablen bezeichnet. Ein Lambda-Ausdruck, der Variablen erfasst, wird als Abschluss bezeichnet.

Letzter zu beachtender Punkt : Erfasste Variablen werden ausgewertet, wenn der Delegat tatsächlich aufgerufen wird, nicht, wenn die Variablen erfasst wurden:

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

0

Wenn Sie eine anonyme Inline-Methode (C # 2) oder (vorzugsweise) einen Lambda-Ausdruck (C # 3 +) schreiben, wird noch eine tatsächliche Methode erstellt. Wenn dieser Code eine lokale Variable im äußeren Bereich verwendet, müssen Sie diese Variable trotzdem irgendwie an die Methode übergeben.

Nehmen Sie z. B. diese Linq Where-Klausel (eine einfache Erweiterungsmethode, die einen Lambda-Ausdruck übergibt):

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

Wenn Sie i in diesem Lambda-Ausdruck verwenden möchten, müssen Sie es an diese erstellte Methode übergeben.

Die erste Frage, die sich stellt, lautet: Soll sie als Wert oder Referenz übergeben werden?

Referenzübergabe ist (ich denke) vorzuziehen, da Sie Lese- / Schreibzugriff auf diese Variable erhalten (und dies ist, was C # tut; ich denke, das Team in Microsoft hat die Vor- und Nachteile abgewogen und sich für Referenzangaben entschieden; laut Jon Skeet Artikel , Java ging mit By-Value).

Aber dann stellt sich eine andere Frage: Wo soll ich das zuordnen?

Sollte es tatsächlich / natürlich auf dem Stapel zugeordnet werden? Wenn Sie es dem Stapel zuweisen und als Referenz übergeben, kann es Situationen geben, in denen es seinen eigenen Stapelrahmen überlebt. Nehmen Sie dieses Beispiel:

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

Der Lambda-Ausdruck (in der Where-Klausel) erstellt erneut eine Methode, die auf ein i verweist. Wenn i auf dem Stapel von Outlive zugewiesen ist, zeigt das in der generierten Methode verwendete i zum Zeitpunkt der Aufzählung der whereItems auf das i von Outlive, dh auf eine Stelle im Stapel, auf die nicht mehr zugegriffen werden kann.

Ok, dann brauchen wir es auf dem Haufen.

Der C # -Compiler unterstützt dieses Inline-Anonym / Lambda mit der Bezeichnung " Closures ": Er erstellt auf dem Heap eine Klasse namens ( ziemlich schlecht ) DisplayClass, die ein Feld enthält, das das i und die tatsächlich verwendete Funktion enthält es.

Etwas, das dem äquivalent wäre (Sie können die mit ILSpy oder ILDASM generierte IL sehen):

class <>c_DisplayClass1
{
    public int i;

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

Es instanziiert diese Klasse in Ihrem lokalen Bereich und ersetzt jeden Code, der sich auf i oder den Lambda-Ausdruck bezieht, durch diese Abschlussinstanz. Wenn Sie also das i in Ihrem "lokalen Bereich" -Code verwenden, in dem ich definiert wurde, verwenden Sie tatsächlich dieses DisplayClass-Instanzfeld.

Wenn ich also das "lokale" i in der Hauptmethode ändern würde, würde es tatsächlich _DisplayClass.i ändern;

dh

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

es wird 12 ausgedruckt, wenn "i = 10" zu diesem Disclalyclass-Feld geht und es kurz vor der 2. Aufzählung ändert.

Eine gute Quelle zu diesem Thema ist dieses Bart De Smet Pluralsight-Modul (Registrierung erforderlich) (ignorieren Sie auch seine fehlerhafte Verwendung des Begriffs "Heben" - was (ich denke) bedeutet, dass die lokale Variable (dh i) geändert wird, um darauf zu verweisen in das neue DisplayClass-Feld).


In anderen Nachrichten scheint es ein Missverständnis zu geben, dass "Closures" mit Schleifen zusammenhängen - wie ich verstehe, sind "Closures" KEIN Konzept, das sich auf Schleifen bezieht , sondern auf anonyme Methoden / Lambda-Ausdrücke, die Variablen mit lokalem Gültigkeitsbereich verwenden - obwohl dies ein Trick ist Fragen verwenden Schleifen, um dies zu demonstrieren.


-1

Ein Abschluss ist eine Funktion, die innerhalb einer Funktion definiert ist und auf die lokalen Variablen sowie auf deren übergeordnete Funktion zugreifen kann.

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

also die Funktion innerhalb der find-Methode.

 t => t.Name == name

kann auf die Variablen in seinem Bereich t und auf den Variablennamen zugreifen, der sich in seinem übergeordneten Bereich befindet. Obwohl es von der find-Methode als Delegat ausgeführt wird, aus einem anderen Bereich insgesamt.


2
Ein Abschluss ist an sich keine Funktion, sondern wird eher durch den Umfang als durch Funktionen definiert. Funktionen helfen lediglich dabei, den Umfang beizubehalten, wodurch ein Abschluss erstellt wird. Aber zu sagen, dass ein Verschluss eine Funktion ist, ist technisch nicht korrekt. Tut mir leid, nicht zu picken. :)
Jason Bunting
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.