it-swarm-eu.dev

Warum sollte ein Programm einen Abschluss verwenden?

Nachdem ich viele Beiträge gelesen habe, in denen Schließungen hier erklärt werden, fehlt mir immer noch ein Schlüsselkonzept: Warum eine Schließung schreiben? Welche spezifische Aufgabe würde ein Programmierer ausführen? am besten durch eine Schließung serviert?

Beispiele für Schließungen in Swift sind Zugriffe auf eine NSUrl und die Verwendung des Reverse-Geocoders. Hier ist ein solches Beispiel. Leider stellen diese Kurse nur die Schließung dar; sie tun dies nicht Erklären Sie, warum die Codelösung als Abschluss geschrieben wird.

Ein Beispiel für ein real world Programmierproblem, das mein Gehirn dazu veranlassen könnte, zu sagen: "Aha, ich sollte einen Abschluss dafür schreiben", wäre informativer als eine theoretische Diskussion. An theoretischen Diskussionen auf dieser Website mangelt es nicht.

61
Bendrix

Erstens gibt es nichts, was ohne Verschlüsse unmöglich ist. Sie können einen Abschluss jederzeit durch ein Objekt ersetzen, das eine bestimmte Schnittstelle implementiert. Es ist nur eine Frage der Kürze und der reduzierten Kopplung.

Denken Sie zweitens daran, dass Verschlüsse häufig unangemessen verwendet werden, wenn eine einfache Funktionsreferenz oder ein anderes Konstrukt klarer wäre. Sie sollten nicht jedes Beispiel als Best Practice ansehen.

Wo Verschlüsse wirklich über anderen Konstrukten glänzen, ist bei Verwendung von Funktionen höherer Ordnung, wenn Sie tatsächlich benötigen, um den Status zu kommunizieren, und Sie können ihn zu einem Einzeiler machen , wie in diesem JavaScript-Beispiel aus der Wikipedia-Seite zu Schließungen :

// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Hier wird threshold sehr prägnant und natürlich von der Definition bis zur Verwendung kommuniziert. Sein Umfang ist genau so klein wie möglich begrenzt. filter muss nicht geschrieben werden, um die Möglichkeit zu ermöglichen, vom Client definierte Daten wie einen Schwellenwert zu übergeben. Wir müssen keine Zwischenstrukturen definieren, um die Schwelle in dieser einen kleinen Funktion zu kommunizieren. Es ist völlig in sich geschlossen.

Sie können dies ohne Abschluss schreiben, aber es erfordert viel mehr Code und ist schwieriger zu befolgen. Außerdem hat JavaScript eine ziemlich ausführliche Lambda-Syntax. In Scala wäre zum Beispiel der gesamte Funktionskörper:

bookList filter (_.sales >= threshold)

Wenn Sie jedoch ECMAScript 6 verwenden können, wird dank der Fettpfeil-Funktionen sogar der JavaScript-Code viel einfacher und kann tatsächlich in eine einzelne Zeile eingefügt werden.

const bestSellingBooks = (threshold) => bookList.filter(book => book.sales >= threshold);

Suchen Sie in Ihrem eigenen Code nach Orten, an denen Sie viel Boilerplate generieren, um temporäre Werte von einem Ort zum anderen zu kommunizieren. Dies sind hervorragende Möglichkeiten, einen Ersatz durch einen Verschluss in Betracht zu ziehen.

33
Karl Bielefeldt

Zur Erklärung leihe ich mir Code von diesem ausgezeichneten Blog-Beitrag über Schließungen . Es ist JavaScript, aber das ist die Sprache, die in den meisten Blog-Posts über Schließungen verwendet wird, da Schließungen in JavaScript so wichtig sind.

Angenommen, Sie wollten ein Array als HTML-Tabelle rendern. Sie könnten es so machen:

function renderArrayAsHtmlTable (array) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + object + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Sie sind jedoch JavaScript ausgeliefert, wie jedes Element im Array gerendert wird. Wenn Sie das Rendern steuern möchten, können Sie Folgendes tun:

function renderArrayAsHtmlTable (array, renderer) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + renderer(object) + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Und jetzt können Sie einfach eine Funktion übergeben, die das gewünschte Rendering zurückgibt.

Was ist, wenn Sie in jeder Tabellenzeile eine laufende Summe anzeigen möchten? Sie würden eine Variable benötigen, um diese Summe zu verfolgen, nicht wahr? Mit einem Abschluss können Sie eine Renderer-Funktion schreiben, die über die Variable für die laufende Summe schließt, und Sie können einen Renderer schreiben, der die laufende Summe verfolgen kann:

function intTableWithTotals (intArray) {
  var total = 0;
  var renderInt = function (i) {
    total += i;
    return "Int: " + i + ", running total: " + total;
  };
  return renderObjectsInTable(intArray, renderInt);
}

Die Magie, die hier geschieht, ist, dass renderInt den Zugriff auf die Variable total behält, obwohl renderInt wiederholt aufgerufen wird und beendet wird.

In einer traditionell objektorientierten Sprache als JavaScript können Sie eine Klasse schreiben, die diese Gesamtvariable enthält, und diese weitergeben, anstatt einen Abschluss zu erstellen. Ein Verschluss ist jedoch eine viel leistungsstärkere, sauberere und elegantere Methode.

Weiterführende Literatur

52
Robert Harvey

Der Zweck von closures besteht einfach darin, zu bewahren state; daher der Name closure - it schließt over state. Zur Erleichterung der weiteren Erklärung verwende ich Javascript.

Normalerweise haben Sie eine Funktion

function sayHello(){
    var txt="Hello";
    return txt;
}

wobei der Umfang der Variablen an diese Funktion gebunden ist. Nach der Ausführung verlässt die Variable txt den Gültigkeitsbereich. Es gibt keine Möglichkeit, nach Abschluss der Ausführung auf die Funktion zuzugreifen oder sie zu verwenden.

Closures sind Sprachkonstrukte, die es - wie bereits erwähnt - ermöglichen, den Status der Variablen beizubehalten und so den Umfang zu verlängern.

Dies kann in verschiedenen Fällen nützlich sein. Ein Anwendungsfall ist die Konstruktion von Funktionen höherer Ordnung .

In der Mathematik und Informatik ist eine Funktion höherer Ordnung (auch funktionale Form, Funktion oder Funktor) eine Funktion, die mindestens eine der folgenden Aufgaben erfüllt: 1

  • nimmt eine oder mehrere Funktionen als Eingabe
  • gibt eine Funktion aus

Ein einfaches, aber zugegebenermaßen nicht allzu nützliches Beispiel ist:

 makeadder=function(a){
     return function(b){
         return a+b;
     }
 }

 add5=makeadder(5);
 console.log(add5(10)); 

Sie definieren eine Funktion makedadder, die einen Parameter als Eingabe verwendet und eine Funktion zurückgibt. Es gibt eine äußere Funktion function(a){} und eine innerefunction(b){}{}. Weiter definieren Sie (implizit) eine andere Funktion add5 Als Ergebnis des Aufrufs der Funktion höherer Ordnung makeadder. makeadder(5) gibt eine anonyme ( inner) Funktion zurück, die wiederum 1 Parameter annimmt und die Summe der Parameter der äußere Funktion zurückgibt und der Parameter der Funktion inner.

Der Trick ist, dass bei der Rückgabe der Funktion inner, die das eigentliche Hinzufügen übernimmt, der Umfang des Parameters der äußeren Funktion (a) bleibt erhalten. add5 erinnert sich, dass der Parameter a5 War.

Oder um ein zumindest irgendwie nützliches Beispiel zu zeigen:

  makeTag=function(openTag, closeTag){
     return function(content){
         return openTag +content +closeTag;
     }
 }

 table=makeTag("<table>","</table>")
 tr=makeTag("<tr>", "</tr>");
 td=makeTag("<td>","</td>");
 console.log(table(tr(td("I am a Row"))));

Ein weiterer häufiger Anwendungsfall ist der sogenannte IIFE = sofort aufgerufene Funktionsausdruck. In Javascript ist es sehr häufig, fake private Mitgliedsvariablen zu verwenden. Dies erfolgt über eine Funktion, die einen privat Bereich = closure erstellt, da dieser unmittelbar nach dem Aufruf der Definition aufgerufen wird. Die Struktur ist function(){}(). Beachten Sie die Klammern () Nach der Definition. Dies ermöglicht die Verwendung für die Objekterstellung mit Aufdecken des Modulmusters . Der Trick besteht darin, einen Bereich zu erstellen und ein Objekt zurückzugeben, das nach Ausführung des IIFE Zugriff auf diesen Bereich hat.

Addis Beispiel sieht folgendermaßen aus:

 var myRevealingModule = (function () {

         var privateVar = "Ben Cherry",
             publicVar = "Hey there!";

         function privateFunction() {
             console.log( "Name:" + privateVar );
         }

         function publicSetName( strName ) {
             privateVar = strName;
         }

         function publicGetName() {
             privateFunction();
         }


         // Reveal public pointers to
         // private functions and properties

         return {
             setName: publicSetName,
             greeting: publicVar,
             getName: publicGetName
         };

     })();

 myRevealingModule.setName( "Paul Kinlan" );

Das zurückgegebene Objekt enthält Verweise auf Funktionen (z. B. publicSetName), die wiederum Zugriff auf "private" Variablen privateVar haben.

Dies sind jedoch speziellere Anwendungsfälle für Javascript.

Welche spezifische Aufgabe würde ein Programmierer ausführen, die am besten durch einen Abschluss erfüllt werden könnte?

Dafür gibt es mehrere Gründe. Man könnte sein, dass es für ihn natürlich ist, da er einem funktionalen Paradigma folgt. Oder in Javascript: es ist nur eine Notwendigkeit Sich auf Verschlüsse zu verlassen, um einige Macken der Sprache zu umgehen.

23
Thomas Junk

Es gibt zwei Hauptanwendungsfälle für Verschlüsse:

  1. Asynchronität. Angenommen, Sie möchten eine Aufgabe ausführen, die eine Weile dauert, und dann etwas tun, wenn sie erledigt ist. Sie können Ihren Code entweder warten lassen, bis er ausgeführt wird, wodurch die weitere Ausführung blockiert wird und Ihr Programm nicht mehr reagiert, oder Sie können Ihre Aufgabe asynchron aufrufen und sagen: "Beginnen Sie diese lange Aufgabe im Hintergrund, und führen Sie diesen Abschluss aus, wenn sie abgeschlossen ist." Dabei enthält der Abschluss den Code, der ausgeführt werden soll, wenn er abgeschlossen ist.

  2. Rückrufe. Diese werden je nach Sprache und Plattform auch als "Delegaten" oder "Ereignishandler" bezeichnet. Die Idee ist, dass Sie ein anpassbares Objekt haben, das an bestimmten genau definierten Punkten ein Ereignis ausführt, das einen vom Code übergebenen Abschluss ausführt das richtet es ein. Beispielsweise haben Sie in der Benutzeroberfläche Ihres Programms möglicherweise eine Schaltfläche und schließen sie, die den Code enthält, der ausgeführt werden soll, wenn der Benutzer auf die Schaltfläche klickt.

Es gibt verschiedene andere Verwendungszwecke für Verschlüsse, aber dies sind die beiden wichtigsten.

16
Mason Wheeler

Ein paar andere Beispiele:

Sortieren
Die meisten Sortierfunktionen vergleichen Objektpaare. Einige Vergleichstechniken sind erforderlich. Die Beschränkung des Vergleichs auf einen bestimmten Operator bedeutet eine ziemlich unflexible Sortierung. Ein viel besserer Ansatz besteht darin, eine Vergleichsfunktion als Argument für die Sortierfunktion zu erhalten. Manchmal funktioniert eine zustandslose Vergleichsfunktion einwandfrei (z. B. Sortieren einer Liste von Zahlen oder Namen), aber was ist, wenn der Vergleich den Status benötigt?

Sie können beispielsweise eine Liste von Städten nach Entfernung zu einem bestimmten Ort sortieren. Eine hässliche Lösung besteht darin, die Koordinaten dieses Ortes in einer globalen Variablen zu speichern. Dies macht die Vergleichsfunktion selbst zustandslos, jedoch auf Kosten einer globalen Variablen.

Dieser Ansatz schließt aus, dass mehrere Threads gleichzeitig dieselbe Liste von Städten nach ihrer Entfernung zu zwei verschiedenen Orten sortieren. Ein Abschluss, der den Speicherort einschließt, löst dieses Problem und macht eine globale Variable überflüssig.


Zufallszahlen
Die ursprüngliche Rand() hat keine Argumente angenommen. Pseudozufallszahlengeneratoren benötigen den Status. Einige (z. B. Mersenne Twister) benötigen viel Zustand. Sogar der einfache, aber schreckliche Rand() benötigte Zustand. Wenn Sie ein Mathejournal über einen neuen Zufallszahlengenerator lesen, werden Sie unweigerlich globale Variablen sehen. Das ist schön für die Entwickler der Technik, nicht so schön für die Anrufer. Das Einkapseln dieses Zustands in eine Struktur und das Übergeben der Struktur an den Zufallszahlengenerator ist eine Möglichkeit, das globale Datenproblem zu umgehen. Dies ist der Ansatz, der in vielen Nicht-OO-Sprachen verwendet wird, um einen Zufallszahlengenerator wiedereintrittsfähig zu machen. Ein Abschluss verbirgt diesen Status vor dem Anrufer. Ein Abschluss bietet die einfache Aufrufsequenz von Rand() und die Wiedereintrittsfähigkeit des eingekapselten Zustands.

Zufallszahlen sind mehr als nur ein PRNG. Die meisten Leute, die Zufälligkeit wollen, wollen, dass es auf eine bestimmte Weise verteilt wird. Ich beginne mit Zahlen, die zufällig zwischen 0 und 1 gezogen werden, oder kurz U (0,1). Jedes PRNG, das Ganzzahlen zwischen 0 und einem Maximum generiert, reicht aus; dividieren Sie einfach (als Gleitkomma) die zufällige Ganzzahl durch das Maximum. Eine bequeme und generische Möglichkeit, dies zu implementieren, besteht darin, einen Abschluss zu erstellen Das erfordert einen Abschluss (PRNG) und das Maximum als Eingaben. Jetzt haben wir einen generischen und einfach zu verwendenden Zufallsgenerator für U (0,1).

Neben U (0,1) gibt es eine Reihe weiterer Verteilungen. Zum Beispiel eine Normalverteilung mit einem bestimmten Mittelwert und einer bestimmten Standardabweichung. Jeder Normalverteilungsgenerator-Algorithmus, auf den ich gestoßen bin, verwendet einen U (0,1) -Generator. Eine bequeme und generische Möglichkeit, einen normalen Generator zu erstellen, besteht darin, einen Abschluss zu erstellen, der den U (0,1) -Generator, den Mittelwert und die Standardabweichung als Zustand kapselt. Dies ist zumindest konzeptionell ein Abschluss, der einen Abschluss als Argument verwendet.

13
David Hammen

Closures entsprechen Objekten, die eine run () -Methode implementieren, und umgekehrt können Objekte mit Closures emuliert werden.

  • Der Vorteil von Verschlüssen besteht darin, dass sie überall dort problemlos verwendet werden können, wo Sie eine Funktion erwarten: a.k.a. Funktionen höherer Ordnung, einfache Rückrufe (oder Strategiemuster). Sie müssen keine Schnittstelle/Klasse definieren, um Ad-hoc-Abschlüsse zu erstellen.

  • Der Vorteil von Objekten ist die Möglichkeit komplexerer Interaktionen: mehrere Methoden und/oder verschiedene Schnittstellen.

Die Verwendung von Verschlüssen oder Objekten ist also meistens eine Frage des Stils. Hier ist ein Beispiel für Dinge, die das Schließen einfach macht, deren Implementierung mit Objekten jedoch unpraktisch ist:

 (let ((seen))
    (defun register-name (name)
       (pushnew name seen :test #'string=))

    (defun all-names ()
       (copy-seq seen))

    (defun reset-name-registry ()
       (setf seen nil)))

Grundsätzlich kapseln Sie einen verborgenen Status, auf den nur über globale Schließungen zugegriffen wird: Sie müssen sich nicht auf ein Objekt beziehen, sondern nur das durch die drei Funktionen definierte Protokoll verwenden.


Ich vertraue dem ersten Kommentar von supercat zu der Tatsache, dass es in einigen Sprachen möglich ist, die Lebensdauer von Objekten genau zu steuern, während das Gleiche nicht für Verschlüsse gilt. Im Fall von durch Müll gesammelten Sprachen ist die Lebensdauer von Objekten jedoch im Allgemeinen unbegrenzt, und es ist daher möglich, einen Abschluss zu erstellen, der in einem dynamischen Kontext aufgerufen werden kann, in dem er nicht aufgerufen werden sollte (Lesen aus einem Abschluss nach einem Stream) ist zum Beispiel geschlossen).

Es ist jedoch recht einfach, einen solchen Missbrauch zu verhindern, indem eine Steuervariable erfasst wird, die die Ausführung eines Verschlusses schützt. Genauer gesagt, hier ist, was ich vorhabe (in Common LISP):

(defun guarded (function)
  (let ((active t))
    (values (lambda (&rest args)
              (when active
                (apply function args)))
            (lambda ()
              (setf active nil)))))

Hier nehmen wir einen Funktionsbezeichner function und geben zwei Abschlüsse zurück, die beide eine lokale Variable mit dem Namen active erfassen:

  • der erste delegiert nur dann an function, wenn active wahr ist
  • der zweite setzt action auf nil, a.k.a. false.

Anstelle von (when active ...) Ist es natürlich möglich, einen (assert active) - Ausdruck zu verwenden, der eine Ausnahme auslösen kann, falls der Abschluss aufgerufen wird, wenn dies nicht der Fall sein sollte. Denken Sie auch daran, dass der unsichere Code möglicherweise bereits eine Ausnahme auslöst von selbst, wenn er schlecht verwendet wird, sodass Sie selten einen solchen Wrapper benötigen.

So würden Sie es verwenden:

(use-package :metabang-bind) ;; for bind

(defun example (obj1 obj2)
  (bind (((:values f f-deactivator)(guarded (lambda () (do-stuff obj1))))
         ((:values g g-deactivator)(guarded (lambda () (do-thing obj2)))))

    ;; ensure the closure are inactive when we exit
    (unwind-protect
         ;; pass closures to other functions
         (progn
           (do-work f)
           (do-work g))

      ;; cleanup code: deactivate closures
      (funcall f-deactivator)
      (funcall g-deactivator))))

Beachten Sie, dass die desaktivierenden Verschlüsse auch anderen Funktionen zugewiesen werden können. Hier werden die lokalen Variablen active nicht zwischen f und g geteilt. Zusätzlich zu active bezieht sich f nur auf obj1 und g nur auf obj2.

Der andere von Supercat erwähnte Punkt ist, dass Schließungen zu Speicherlecks führen können, aber leider ist dies bei fast allem in Umgebungen mit Müllsammlung der Fall. Wenn sie verfügbar sind, kann dies durch schwache Zeiger behoben werden (der Abschluss selbst wird möglicherweise im Speicher gespeichert, verhindert jedoch nicht die Speicherbereinigung anderer Ressourcen ).

7
coredump

Nichts, was noch nicht gesagt wurde, aber vielleicht ein einfacheres Beispiel.

Hier ist ein JavaScript-Beispiel mit Timeouts:

// Example function that logs something to the browser's console after a given delay
function delayedLog(message, delay) {
  // this function will be called when the timer runs out
  var fire = function () {
    console.log(message); // closure magic!
  };

  // set a timeout that'll call fire() after a delay
  setTimeout(fire, delay);
}

Was hier passiert, ist, dass wenn delayedLog() aufgerufen wird, es unmittelbar nach dem Festlegen des Timeouts zurückkehrt und das Timeout im Hintergrund weiter abläuft.

Wenn das Zeitlimit jedoch abgelaufen ist und die Funktion fire() aufgerufen wird, zeigt die Konsole das message an, das ursprünglich an delayedLog() übergeben wurde, da es für fire() über Schließung. Sie können delayedLog() so oft aufrufen, wie Sie möchten, mit einer anderen Meldung und Verzögerung jedes Mal, und es wird das Richtige tun.

Stellen wir uns jedoch vor, JavaScript hat keine Verschlüsse.

Eine Möglichkeit wäre, setTimeout() zu blockieren - eher wie eine "Schlaf" -Funktion - damit der Gültigkeitsbereich von delayedLog() nicht verschwindet, bis das Timeout abgelaufen ist. Aber alles zu blockieren ist nicht sehr schön.

Eine andere Möglichkeit wäre, die Variable message in einen anderen Bereich zu verschieben, auf den zugegriffen werden kann, nachdem der Bereich von delayedLog() weg ist.

Sie könnten globale - oder zumindest "breitere" - Variablen verwenden, aber Sie müssten herausfinden, wie Sie verfolgen können, welche Nachricht zu welchem ​​Timeout gehört. Es kann aber nicht nur eine sequentielle Warteschlange sein, FIFO Warteschlange), da Sie jede gewünschte Verzögerung einstellen können. Es kann sich also also um "First In, Third Out" oder etwas anderes handeln Ein anderes Mittel, um eine zeitgesteuerte Funktion an die benötigten Variablen zu binden.

Sie können ein Timeout-Objekt instanziieren, das den Timer mit der Nachricht "gruppiert". Der Kontext eines Objekts ist mehr oder weniger ein Bereich, der herumsteht. Dann müsste der Timer im Kontext des Objekts ausgeführt werden, damit er Zugriff auf die richtige Nachricht hat. Aber Sie müssten dieses Objekt speichern, da es ohne Referenzen Müll sammeln würde (ohne Verschlüsse gäbe es auch keine impliziten Verweise darauf). Und Sie müssten das Objekt entfernen, sobald es seine Zeitüberschreitung ausgelöst hat, sonst bleibt es einfach hängen. Sie benötigen also eine Art Liste von Timeout-Objekten und überprüfen diese regelmäßig auf "verbrauchte" Objekte, die entfernt werden sollen. Andernfalls fügen sich die Objekte hinzu und entfernen sich aus der Liste.

Also ... ja, das wird langweilig.

Zum Glück müssen Sie keinen breiteren Bereich verwenden oder Objekte streiten, um bestimmte Variablen beizubehalten. Da JavaScript über Schließungen verfügt, haben Sie bereits genau den Umfang, den Sie benötigen. Ein Bereich, mit dem Sie bei Bedarf auf die Variable message zugreifen können. Und aus diesem Grund können Sie delayedLog() wie oben schreiben.

6
Flambino

PHP kann verwendet werden, um ein reales Beispiel in einer anderen Sprache zu zeigen.

protected function registerRoutes($dic)
{
  $router = $dic['router'];

  $router->map(['GET','OPTIONS'],'/api/users',function($request,$response) use ($dic)
  {
    $controller = $dic['user_api_controller'];
    return $controller->findAllAction($request,$response);
  })->setName('api_users');
}

Im Grunde genommen registriere ich eine Funktion, die für die/api/users ausgeführt wird URI . Dies ist eigentlich eine Middleware-Funktion, die auf einem Stapel gespeichert wird. Andere Funktionen werden darum herum gewickelt. Ziemlich ähnlich wie Node.js / Express.js .

Der Container Abhängigkeitsinjektion ist (über die use-Klausel) in der Funktion verfügbar, wenn er aufgerufen wird. Es ist möglich, eine Art Routenaktionsklasse zu erstellen, aber dieser Code ist einfacher, schneller und einfacher zu warten.

3
Cerad