Dlaczego program miałby używać zamknięcia?


58

Po przeczytaniu wielu postów wyjaśniających zamknięcie tutaj wciąż brakuje mi kluczowej koncepcji: po co pisać zamknięcie? Jakie konkretne zadanie wykonałby programista, któremu najlepiej byłoby zamknąć?

Przykłady zamknięć w Swift to dostęp do NSUrl i użycie odwrotnego geokodera. Oto jeden taki przykład. Niestety te kursy przedstawiają tylko zamknięcie; nie wyjaśniają, dlaczego rozwiązanie kodu jest napisane jako zamknięcie.

Przykład problemu programowania w świecie rzeczywistym, który może sprawić, że mój mózg powie „aha, powinienem napisać zakończenie tego”, byłby bardziej pouczający niż dyskusja teoretyczna. Na tej stronie nie brakuje dyskusji teoretycznych.


7
„Niedawno sprawdzone wyjaśnienie zamknięć tutaj” - brakuje Ci linku?
Dan Pichelman

2
Wyjaśnienie dotyczące zadania asynchronicznego należy umieścić w komentarzu poniżej odpowiedniej odpowiedzi. To nie jest część twojego pierwotnego pytania, a osoba odpowiadająca nigdy nie otrzyma powiadomienia o Twojej edycji.
Robert Harvey

5
Tylko ciekawostka: najważniejszym punktem Zamknięć jest zakres leksykalny (w przeciwieństwie do zakresu dynamicznego), zamknięcia bez zakresu leksykalnego są raczej bezużyteczne. Zamknięcia są czasami nazywane zamknięciami leksykalnymi.
Hoffmann

1
Zamknięcia umożliwiają tworzenie instancji funkcji .
user253751

1
Przeczytaj dobrą książkę na temat programowania funkcjonalnego. Być może zacznij od SICP
Basile Starynkevitch

Odpowiedzi:


33

Przede wszystkim nie ma nic niemożliwego bez użycia zamknięć. Zawsze możesz zastąpić zamknięcie obiektem implementującym określony interfejs. To tylko kwestia zwięzłości i zmniejszonego sprzężenia.

Po drugie, należy pamiętać, że zamknięcia są często używane niewłaściwie, w przypadku gdy proste odniesienie do funkcji lub inna konstrukcja byłaby bardziej przejrzysta. Nie powinieneś brać każdego przykładu, który postrzegasz jako najlepszą praktykę.

Tam, gdzie zamknięcia naprawdę świecą nad innymi konstrukcjami, gdy używasz funkcji wyższego rzędu, kiedy faktycznie potrzebujesz komunikować stan, i możesz uczynić go jednowierszowym, jak w tym przykładzie JavaScript ze strony wikipedia na temat zamknięć :

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

Tutaj thresholdjest bardzo zwięźle i naturalnie przekazywane z miejsca, w którym jest zdefiniowane, do miejsca, w którym jest używane. Jego zakres jest dokładnie tak mały, jak to możliwe. filternie trzeba pisać, aby umożliwić przekazanie danych zdefiniowanych przez klienta, takich jak próg. Nie musimy definiować żadnych struktur pośrednich wyłącznie w celu przekazania progu w tej jednej małej funkcji. Jest całkowicie samowystarczalny.

Możesz to napisać bez zamknięcia, ale będzie to wymagało znacznie więcej kodu i będzie trudniejsze do naśladowania. Ponadto JavaScript ma dość pełną składnię lambda. Na przykład w Scali całe ciało funkcji byłoby:

bookList filter (_.sales >= threshold)

Jeśli jednak możesz używać ECMAScript 6 , dzięki funkcjom grubej strzałki nawet kod JavaScript staje się znacznie prostszy i można go umieścić w jednym wierszu.

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

W swoim własnym kodzie szukaj miejsc, w których generujesz dużą liczbę podstawek, aby przekazywać wartości tymczasowe z jednego miejsca do drugiego. Są to doskonałe możliwości rozważenia zastąpienia zamknięciem.


Jak dokładnie zamknięcia zmniejszają sprzężenie?
sbichenko

Bez zamknięć musisz nałożyć ograniczenia zarówno na bestSellingBookskod, jak i na filterkod, takie jak określony interfejs lub argument danych użytkownika, aby móc komunikować się z thresholddanymi. To łączy te dwie funkcje razem w znacznie mniej wielokrotny sposób.
Karl Bielefeldt,

51

Dla wyjaśnienia, pożyczę trochę kodu z tego doskonałego postu na blogu o zamknięciach . Jest to JavaScript, ale jest to język, w którym większość postów na blogu mówi o zamknięciach, ponieważ zamknięcia są tak ważne w JavaScript.

Powiedzmy, że chcesz renderować tablicę jako tabelę HTML. Możesz to zrobić w następujący sposób:

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

Ale jesteś na łasce JavaScript, w jaki sposób każdy element w tablicy będzie renderowany. Jeśli chcesz kontrolować renderowanie, możesz to zrobić:

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

A teraz możesz po prostu przekazać funkcję, która zwraca żądany rendering.

Co jeśli chcesz wyświetlać sumę bieżącą w każdym wierszu tabeli? Potrzebujesz zmiennej do śledzenia tej sumy, prawda? Zamknięcie pozwala napisać funkcję renderera, która zamyka działającą zmienną total, i pozwala napisać renderer, który może śledzić działającą sumę:

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

Magia, która się tutaj dzieje, polega na tym, że renderInt zachowuje dostęp do totalzmiennej, nawet jeśli renderIntjest wielokrotnie wywoływana i wychodzi.

W języku bardziej tradycyjnie zorientowanym obiektowo niż JavaScript można napisać klasę zawierającą tę zmienną całkowitą i przekazać ją zamiast tworzyć zamknięcie. Ale zamknięcie jest znacznie bardziej wydajnym, czystym i eleganckim sposobem na zrobienie tego.

Dalsza lektura


10
Ogólnie można powiedzieć, że „funkcja pierwszej klasy == obiekt z tylko jedną metodą”, „Zamknięcie == obiekt z tylko jedną metodą i stanem”, „obiekt == pakiet zamknięć”.
Jörg W Mittag,

Wygląda dobrze. Inną rzeczą, a to może być pedanterii, jest to, że JavaScript jest obiektowy, a mógłby stworzyć obiekt, który zawiera zmienną całkowitą i przekazać, że ok. Zamknięcia pozostają o wiele potężniejsze, czyste i eleganckie, a także sprawiają, że JavaScript jest znacznie bardziej idiomatyczny, ale sposób, w jaki jest napisany, może sugerować, że JavaScript nie może tego robić w sposób obiektowy.
KRyan

2
W rzeczywistości, aby być naprawdę pedantycznym: zamknięcia są tym, co sprawia, że ​​JavaScript jest zorientowany obiektowo! OO dotyczy abstrakcji danych, a zamknięcia są sposobem wykonywania abstrakcji danych w JavaScript.
Jörg W Mittag

@ JörgWMittag: Podobnie jak możesz zobaczyć skupiska jako obiekty za pomocą tylko jednej metody, możesz zobaczyć obiekty jako kolekcję zamknięć, które zamykają te same zmienne: zmienne składowe obiektu są tylko lokalnymi zmiennymi konstruktora obiektu oraz metodami obiektu są zamknięciami zdefiniowanymi w zakresie konstruktora i udostępnionymi do późniejszego wywołania. Funkcja konstruktora zwraca funkcję wyższego rzędu (obiekt), która może wywołać każde zamknięcie zgodnie z nazwą metody używanej do wywołania.
Giorgio,

@Giorgio: Rzeczywiście, tak na przykład obiekty są zazwyczaj implementowane w Schemacie, a tak naprawdę są one również implementowane w JavaScript (nic dziwnego, biorąc pod uwagę jego bliski związek z Schemem, w końcu Brendan Eich został pierwotnie zatrudniony do zaprojektowania Schemat dialektu i implementacja wbudowanego interpretera schematu w Netscape Navigator, a dopiero później zlecono mu stworzenie języka „z obiektami wyglądającymi jak C ++”, po czym dokonał absolutnie minimalnej ilości zmian, aby spełnić te wymagania marketingowe).
Jörg W Mittag,

22

Celem closuresjest po prostu zachowanie stanu; stąd nazwa closure- zamyka się nad stanem. Dla ułatwienia dalszych wyjaśnień użyję Javascript.

Zazwyczaj masz funkcję

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

gdzie zakres zmiennych jest związany z tą funkcją. Po wykonaniu zmienna txtwychodzi poza zakres. Po zakończeniu wykonywania funkcji nie ma możliwości uzyskania do niej dostępu ani korzystania z niej.

Zamknięcia są konstrukcjami językowymi, które pozwalają - jak powiedziano wcześniej - zachować stan zmiennych i tym samym przedłużyć zakres.

Może to być przydatne w różnych przypadkach. Jednym z przypadków użycia jest konstrukcja funkcji wyższego rzędu .

W matematyce i informatyce funkcja wyższego rzędu (także forma funkcjonalna, funkcjonalna lub funktor) to funkcja, która wykonuje co najmniej jedną z następujących czynności: 1

  • przyjmuje jedną lub więcej funkcji jako dane wejściowe
  • wyprowadza funkcję

Prostym, ale co prawda niezbyt przydatnym przykładem jest:

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

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

Definiujesz funkcję makedadder, która przyjmuje jeden parametr jako dane wejściowe i zwraca funkcję . Istnieje funkcja zewnętrznafunction(a){} i wewnętrzna. function(b){}{} Następnie definiujesz (domyślnie) inną funkcję add5w wyniku wywołania funkcji wyższego rzędu makeadder. makeadder(5)zwraca anonimową ( wewnętrzną ) funkcję, która z kolei przyjmuje 1 parametr i zwraca sumę parametru funkcji zewnętrznej i parametru funkcji wewnętrznej .

Trik jest to, że podczas zawracania wewnętrzną funkcję, która nie rzeczywiste dodanie zakres parametru funkcji zewnętrznej ( a) jest zachowana. add5 pamięta , że parametr abył 5.

Lub pokazać przynajmniej jeden przydatny przykład:

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

Innym powszechnym przypadkiem użycia jest tak zwane wyrażenie funkcji IIFE = natychmiast wywołane. W javascript bardzo często fałszowane są prywatne zmienne członkowskie. Odbywa się to za pomocą funkcji, która tworzy prywatny zasięg = closure, ponieważ jest on wywoływany natychmiast po wywołaniu definicji. Struktura jest function(){}(). Zwróć uwagę na nawiasy ()po definicji. Dzięki temu można go używać do tworzenia obiektów z odkrywającym wzorem modułu . Sztuką jest utworzenie zakresu i zwrócenie obiektu, który ma dostęp do tego zakresu po wykonaniu IIFE.

Przykład Addiego wygląda następująco:

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

Zwrócony obiekt ma odwołania do funkcji (np. publicSetName), Które z kolei mają dostęp do zmiennych „prywatnych” privateVar.

Ale są to bardziej szczególne przypadki użycia dla Javascript.

Jakie konkretne zadanie wykonałby programista, któremu najlepiej byłoby zamknąć?

Jest tego kilka przyczyn. Być może jest to dla niego naturalne, ponieważ postępuje według funkcjonalnego paradygmatu . Lub w Javascript: po prostu trzeba polegać na zamknięciach, aby ominąć niektóre dziwactwa języka.


„Celem zamknięć jest po prostu zachowanie stanu; stąd nazwa zamknięcie - zamyka się ponad stan.”: Tak naprawdę zależy to od języka. Zamknięcia zamykają zewnętrzne nazwy / zmienne. Mogą one oznaczać stan (lokalizacje pamięci, które można zmienić), ale mogą także oznaczać wartości (niezmienne). Zamknięcia w Haskell nie zachowują stanu: zachowują informacje, które były znane w danym momencie oraz w kontekście, w którym zamknięcie zostało utworzone.
Giorgio

1
»Zachowują informacje znane w danym momencie oraz w kontekście, w którym zamknięcie zostało utworzone« niezależnie od tego, czy można je zmienić, czy nie, jest to stan - być może stan niezmienny .
Thomas Junk

16

Istnieją dwa główne przypadki użycia dla zamknięć:

  1. Asynchronia. Powiedzmy, że chcesz wykonać zadanie, które zajmie chwilę, a następnie zrób coś po jego zakończeniu. Możesz albo poczekać na wykonanie kodu, co blokuje dalsze wykonywanie i może spowodować, że Twój program przestanie odpowiadać, lub wywołać swoje zadanie asynchronicznie i powiedzieć „rozpocznij to długie zadanie w tle, a po jego zakończeniu wykonaj to zamknięcie”, gdzie zamknięcie zawiera kod do wykonania po zakończeniu.

  2. Callbacki. Są one również znane jako „delegaci” lub „moduły obsługi zdarzeń” w zależności od języka i platformy. Chodzi o to, że masz konfigurowalny obiekt, który w pewnych ściśle określonych punktach wykona zdarzenie , które uruchamia zamknięcie przekazane przez kod, który go konfiguruje. Na przykład w interfejsie programu możesz mieć przycisk i dajesz mu zamknięcie, które zawiera kod do wykonania, gdy użytkownik kliknie ten przycisk.

Istnieje kilka innych zastosowań zamknięć, ale są to dwa główne.


23
Zatem w zasadzie oddzwanianie, ponieważ pierwszy przykład jest również oddzwanianiem.
Robert Harvey

2
@RobertHarvey: Technicznie prawda, ale są to różne modele mentalne. Na przykład (ogólnie) oczekuje się, że moduł obsługi zdarzeń będzie wywoływany wiele razy, ale kontynuacja asynchroniczna będzie wywoływana tylko raz. Ale tak, technicznie wszystko, co zrobiłbyś z zamknięciem, jest oddzwanianiem. (Chyba że przekształcasz je w drzewo wyrażeń, ale to zupełnie inna sprawa.);)
Mason Wheeler

4
@RobertHarvey: Oglądanie zamknięć jako wywołań zwrotnych wprowadza cię w stan umysłu, który naprawdę powstrzyma cię przed ich efektywnym użyciem. Zamknięcia są wywołaniami zwrotnymi na sterydach.
gnasher729

13
@MasonWheeler Mówiąc tak, brzmi to źle. Oddzwanianie nie ma nic wspólnego z zamknięciami; oczywiście możesz oddzwonić do funkcji w zamknięciu lub wywołać zamknięcie jako oddzwonienie; ale oddzwonienie nie jest konieczne zamknięcie. Oddzwonienie to po prostu proste wywołanie funkcji. Moduł obsługi zdarzeń sam w sobie nie jest zamknięciem. Delegat nie jest konieczny do zamknięcia: jest to przede wszystkim sposób C # wskaźników funkcji. Oczywiście możesz go użyć do zbudowania zamknięcia. Twoje wyjaśnienie jest nieprecyzyjne. Chodzi o to, by zamknąć stan i wykorzystać go.
Thomas Junk

5
Być może zachodzą tutaj konotacje specyficzne dla JS, które sprawiają, że nie rozumiem, ale ta odpowiedź brzmi dla mnie całkowicie błędnie. Asynchronia lub wywołania zwrotne mogą wykorzystywać wszystko, co można „wywołać”. Zamknięcie nie jest wymagane dla żadnego z nich - normalna funkcja dobrze się sprawdzi. Tymczasem zamknięcie, zgodnie z definicją w programowaniu funkcjonalnym lub używane w Pythonie, jest przydatne, jak mówi Thomas, ponieważ „zamyka” pewien stan, tj. Zmienną, i daje funkcji w zakresie wewnętrznym spójny dostęp do tej zmiennej wartość, nawet jeśli funkcja jest wywoływana i wychodzi wiele razy.
Jonathan Hartley,

13

Kilka innych przykładów:

Sortowanie
Większość funkcji sortowania działa poprzez porównywanie par obiektów. Potrzebna jest technika porównania. Ograniczenie porównania do konkretnego operatora oznacza raczej mało elastyczny sposób. O wiele lepszym podejściem jest otrzymanie funkcji porównania jako argumentu funkcji sortowania. Czasami funkcja porównywania bezstanowego działa dobrze (np. Sortowanie listy numerów lub nazw), ale co, jeśli porównanie wymaga stanu?

Rozważ na przykład sortowanie listy miast według odległości do określonej lokalizacji. Brzydkim rozwiązaniem jest przechowywanie współrzędnych tej lokalizacji w zmiennej globalnej. To sprawia, że ​​sama funkcja porównania jest bezstanowa, ale kosztem zmiennej globalnej.

Takie podejście wyklucza jednoczesne sortowanie wielu wątków tej samej listy miast według odległości do dwóch różnych lokalizacji. Zamknięcie obejmujące lokalizację rozwiązuje ten problem i pozbywa się potrzeby globalnej zmiennej.


Liczby losowe
Oryginał rand()nie przyjął żadnych argumentów. Generatory liczb pseudolosowych wymagają stanu. Niektóre (np. Mersenne Twister) potrzebują dużo stanu. Nawet prosty, ale strasznie rand()potrzebny stan. Przeczytaj artykuł z matematyki na nowym generatorze liczb losowych, a nieuchronnie zobaczysz zmienne globalne. To miłe dla twórców techniki, nie tak miłe dla osób dzwoniących. Zamknięcie tego stanu w strukturze i przekazanie struktury do generatora liczb losowych jest jednym z rozwiązań problemu globalnych danych. Takie podejście stosuje się w wielu językach innych niż OO, aby ponownie wprowadzić generator liczb losowych. Zamknięcie ukrywa ten stan przed dzwoniącym. Zamknięcie oferuje prostą sekwencję wywoływania rand()i ponowne wprowadzenie stanu enkapsulacji.

Losowe liczby to coś więcej niż tylko PRNG. Większość ludzi, którzy chcą losowości, chce, aby była ona dystrybuowana w określony sposób. Zacznę od liczb losowanych od 0 do 1 lub w skrócie U (0,1). Zrobi to każdy PRNG, który generuje liczby całkowite od 0 do pewnego maksimum; po prostu podziel (jako liczbę zmiennoprzecinkową) losową liczbę całkowitą przez maksimum. Wygodnym i ogólnym sposobem realizacji tego jest utworzenie zamknięcia, które przyjmuje zamknięcie (PRNG) i maksimum jako dane wejściowe. Teraz mamy ogólny i łatwy w użyciu generator losowy dla U (0,1).

Istnieje wiele innych rozkładów oprócz U (0,1). Na przykład rozkład normalny z pewną średnią i odchyleniem standardowym. Każdy normalny algorytm generatora dystrybucji, z którym się zetknąłem, korzysta z generatora U (0,1). Wygodnym i ogólnym sposobem na utworzenie normalnego generatora jest utworzenie zamknięcia, które obudowuje generator U (0,1), średnią i odchylenie standardowe jako stan. Jest to, przynajmniej koncepcyjnie, zamknięcie, które przyjmuje zamknięcie, które przyjmuje zamknięcie jako argument.


7

Zamknięcia są równoważne obiektom implementującym metodę run () i odwrotnie, obiekty mogą być emulowane przy pomocy zamknięć.

  • Zaletą zamknięć jest to, że można ich łatwo używać wszędzie tam, gdzie oczekujesz funkcji: aka funkcji wyższego rzędu, prostych wywołań zwrotnych (lub wzorca strategii). Nie trzeba definiować interfejsu / klasy, aby budować zamknięcia ad-hoc.

  • Zaletą obiektów jest możliwość bardziej złożonych interakcji: wiele metod i / lub różne interfejsy.

Tak więc użycie zamknięcia lub obiektów jest głównie kwestią stylu. Oto przykład rzeczy, które zamknięcia są łatwe, ale są niewygodne w realizacji z obiektami:

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

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

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

Zasadniczo hermetyzujesz stan ukryty, do którego dostęp uzyskuje się tylko poprzez globalne zamknięcia: nie musisz odwoływać się do żadnego obiektu, użyj tylko protokołu zdefiniowanego przez trzy funkcje.


  • Rozszerzam odpowiedź, aby rozwiązać ten komentarz od supercat *.

Ufam pierwszemu komentarzowi supercata o fakcie, że w niektórych językach można dokładnie kontrolować żywotność obiektów, podczas gdy to samo nie dotyczy praw do zamykania. W przypadku języków, w których śmieci są gromadzone, czas życia obiektów jest na ogół nieograniczony, a zatem możliwe jest zbudowanie zamknięcia, które można wywołać w kontekście dynamicznym, w którym nie powinno się go wywoływać (czytanie z zamknięcia po strumieniu jest na przykład zamknięty).

Jednak dość łatwo jest zapobiec takiemu niewłaściwemu użyciu, przechwytując zmienną kontrolną, która będzie pilnować wykonania zamknięcia. Dokładniej, oto co mam na myśli (w Common Lisp):

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

Tutaj bierzemy oznaczenie funkcji functioni zwracamy dwa zamknięcia, oba przechwytujące zmienną lokalną o nazwie active:

  • pierwszy deleguje się functiontylko wtedy, gdy activejest to prawdą
  • drugi ustawia się actionna nilaka false.

Zamiast tego (when active ...)możliwe jest oczywiście (assert active)wyrażenie, które może zgłosić wyjątek w przypadku wywołania zamknięcia, gdy nie powinno. Pamiętaj również, że niebezpieczny kod może już sam rzucić wyjątek , gdy jest źle używany, więc rzadko potrzebujesz takiego opakowania.

Oto, jak byś tego użył:

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

Należy zauważyć, że zamknięcia dezaktywujące można również nadać również innym funkcjom; tutaj activezmienne lokalne nie są współużytkowane przez fi g; Ponadto, dodatkowo do active, fodnosi się tylko do obj1i gdotyczy tylko obj2.

Innym punktem wspomnianym przez supercat jest to, że zamknięcia mogą prowadzić do wycieków pamięci, ale niestety dzieje się tak w przypadku prawie wszystkiego w środowiskach śmieci. Jeśli są dostępne, można to rozwiązać za pomocą słabych wskaźników (samo zamknięcie może być przechowywane w pamięci, ale nie zapobiega odśmiecaniu innych zasobów).


1
Wadą powszechnie stosowanych zamknięć jest to, że gdy zamknięcie, które wykorzystuje jedną ze zmiennych lokalnych metody, zostanie wystawione na świat zewnętrzny, metoda definiująca zamknięcie może stracić kontrolę nad tym, kiedy zmienna jest odczytywana i zapisywana. W większości języków nie ma wygodnego sposobu zdefiniowania efemerycznego zamknięcia, przekazania go do metody i zagwarantowania, że ​​przestanie istnieć po zwróceniu metody otrzymującej, ani też w wielu językach nie ma możliwości gwarantuje, że zamknięcie nie zostanie uruchomione w nieoczekiwanym kontekście wątków.
supercat

@ superupat: spójrz na Objective-C i Swift.
gnasher729

1
@supercat Czy nie byłoby takiej samej wady w przypadku obiektów stanowych?
Andres F.

@AndresF .: Możliwe jest kapsułkowanie obiektu stanowego w opakowaniu, które obsługuje unieważnienie; jeśli kod hermetyzuje prywatną List<T>własność (hipotetyczną klasę) TemporaryMutableListWrapper<T>i wystawia go na działanie kodu zewnętrznego, można zapewnić, że jeśli unieważni opakowanie, kod zewnętrzny nie będzie już mógł manipulować List<T>. Można zaprojektować zamknięcia, aby umożliwić unieważnienie, gdy osiągną zamierzony cel, ale nie jest to wygodne. Istnieją zamknięcia, które sprawiają, że pewne wzory są wygodne, a wysiłek wymagany do ich ochrony zaprzecza temu.
supercat

1
@coredump: W języku C #, jeśli dwa zamknięcia mają wspólne zmienne, ten sam obiekt wygenerowany przez kompilator będzie obsługiwał oba, ponieważ zmiany dokonane we wspólnej zmiennej przez jedno zamknięcie muszą być widoczne przez drugie. Unikanie tego współdzielenia wymagałoby, aby każde zamknięcie było jego własnym obiektem, który zawierał własne niepodzielone zmienne, a także odwołanie do wspólnego obiektu, który przechowuje zmienne współdzielone. Nie jest to niemożliwe, ale spowodowałoby to dodatkowy poziom dereferencji w przypadku większości zmiennych dostępów, spowalniając wszystko.
supercat

6

Nic jeszcze nie zostało powiedziane, ale może prostszy przykład.

Oto przykład JavaScript wykorzystujący limity czasu:

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

To, co się tutaj dzieje, polega na tym, że gdy delayedLog()jest wywoływane, wraca natychmiast po ustawieniu limitu czasu, a limit czasu wciąż odmierza czas w tle.

Ale gdy limit czasu się skończy i fire()wywoła funkcję, konsola wyświetli to, messageco zostało pierwotnie przekazane delayedLog(), ponieważ nadal jest dostępne do fire()zamknięcia. Możesz dzwonić delayedLog()tyle, ile chcesz, z inną wiadomością i opóźniać za każdym razem, a zrobi to dobrze.

Wyobraźmy sobie jednak, że JavaScript nie ma zamknięć.

Jednym ze sposobów byłoby setTimeout()zablokowanie - bardziej jak funkcja „uśpienia” - więc delayedLog()zakres nie zniknie, dopóki nie skończy się limit czasu. Ale blokowanie wszystkiego nie jest zbyt przyjemne.

Innym sposobem byłoby umieszczenie messagezmiennej w innym zasięgu, który będzie dostępny po delayedLog()zniknięciu zakresu.

Możesz użyć zmiennych globalnych - a przynajmniej „szerszego zakresu” - zmiennych, ale musisz dowiedzieć się, jak śledzić, który komunikat idzie z określonym limitem czasu. Ale nie może to być sekwencyjna kolejka FIFO, ponieważ można ustawić dowolne opóźnienie. Może to być „pierwsze wejście, trzecie wyjście” lub coś takiego. Potrzebujesz więc innych sposobów na powiązanie funkcji czasowej ze zmiennymi, których potrzebuje.

Można utworzyć instancję obiektu limitu czasu, który „grupuje” licznik czasu z komunikatem. Kontekst obiektu jest mniej więcej zakresem, który się przylega. Wtedy zegar miałby być wykonywany w kontekście obiektu, aby miał dostęp do właściwej wiadomości. Ale musiałbyś przechowywać ten obiekt, ponieważ bez żadnych odniesień zostałby on wyrzucony śmieci (bez zamknięć, nie byłoby też żadnych ukrytych odniesień do niego). I trzeba będzie usunąć obiekt, gdy upłynie limit czasu, w przeciwnym razie po prostu się zatrzyma. Potrzebna byłaby więc lista obiektów z limitem czasu i okresowo sprawdzana pod kątem „zużytych” obiektów do usunięcia - lub obiekty te dodawałyby i usuwały się z listy i ...

Więc ... tak, robi się nudno.

Na szczęście nie musisz używać szerszego zakresu ani dusić obiektów, aby utrzymać pewne zmienne w pobliżu. Ponieważ JavaScript ma zamknięcia, masz już dokładnie taki zakres, jaki potrzebujesz. Zakres, który daje dostęp do messagezmiennej, gdy jej potrzebujesz. Z tego powodu możesz uciec od pisania delayedLog()jak wyżej.


Mam problem z nazwaniem tego zamknięciem . Być może zaryzykowałbym przypadkowe zamknięcie . messagejest objęty zakresem funkcji firei dlatego jest opisywany w kolejnych wywołaniach; ale robi to przypadkowo tak powiedzieć. Technicznie jest to zamknięcie. +1 tak czy inaczej;)
Thomas Junk

@ThomasJunk Nie jestem pewien, czy całkiem podążam. Jak jest „ non -accidental zamknięcia” wyglądać? Widziałem twój makeadderprzykład powyżej, który moim zdaniem wydaje się taki sam. Zwracamy funkcję „curry”, która przyjmuje jeden argument zamiast dwóch; w ten sam sposób tworzę funkcję, która przyjmuje zero argumentów. Po prostu nie zwracam go, ale przekazuję go setTimeout.
Flambino,

»Po prostu tego nie zwracam« być może to właśnie stanowi dla mnie różnicę. Nie mogę do końca wyrazić mojej „troski”;) Pod względem technicznym masz 100% racji. Odwołanie messagew firegeneruje zamknięcie. A kiedy zostanie wywołany setTimeout, wykorzystuje stan zachowany.
Thomas Junk

1
@ThomasJunk Widzę, jak może pachnieć trochę inaczej. Może jednak nie podzielę się twoją troską :) W każdym razie nie nazwałbym tego „przypadkowym” - całkiem pewne, że celowo to zakodowałem;)
Flambino

3

PHP może pomóc w pokazaniu prawdziwego przykładu w innym języku.

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

Zasadniczo rejestruję więc funkcję, która zostanie wykonana dla identyfikatora URI / api / users . W rzeczywistości jest to funkcja oprogramowania pośredniego, która ostatecznie jest przechowywana na stosie. Inne funkcje zostaną dookoła niego. Zupełnie jak robi to Node.js / Express.js .

Wtrysk zależność pojemnik jest dostępny (poprzez wykorzystanie klauzuli) wewnątrz funkcji, gdy zostanie wywołany. Możliwe jest stworzenie jakiejś klasy akcji trasy, ale ten kod okazuje się prostszy, szybszy i łatwiejszy w utrzymaniu.


-1

Zamknięcie jest kawałek dowolnego kodu, w tym zmiennych, które mogą być traktowane jako dane pierwszej klasy.

Trywialnym przykładem jest stary dobry qsort: jest to funkcja do sortowania danych. Musisz nadać mu wskaźnik do funkcji, która porównuje dwa obiekty. Musisz napisać funkcję. Ta funkcja może wymagać parametryzacji, co oznacza, że ​​podasz jej zmienne statyczne. Co oznacza, że ​​nie jest bezpieczny dla wątków. Jesteś w DS. Więc piszesz alternatywę, która przyjmuje zamknięcie zamiast wskaźnika funkcji. Natychmiast rozwiązujesz problem parametryzacji, ponieważ parametry stają się częścią zamknięcia. Czynisz kod bardziej czytelnym, ponieważ piszesz, jak porównywane są obiekty bezpośrednio z kodem, który wywołuje funkcję sortowania.

Istnieje mnóstwo sytuacji, w których chcesz wykonać pewne czynności, które wymagają dużej ilości kodu płyty kotła, a także jednego małego, ale niezbędnego fragmentu kodu, który należy dostosować. Unikasz kodu tablicy rejestracyjnej, pisząc raz funkcję, która pobiera parametr zamknięcia i wykonuje cały kod tablicy rejestracyjnej wokół niego, a następnie możesz wywołać tę funkcję i przekazać kod, który zostanie dostosowany jako zamknięcie. Bardzo kompaktowy i czytelny sposób pisania kodu.

Masz funkcję, w której trzeba wykonać nietrywialny kod w wielu różnych sytuacjach. Służyło to do tworzenia duplikacji lub zniekształconego kodu, dzięki czemu nietrywialny kod byłby obecny tylko raz. Trywialne: przypisujesz zamknięcie zmiennej i wywołujesz ją w najbardziej oczywisty sposób wszędzie tam, gdzie jest to potrzebne.

Wielowątkowość: iOS / MacOS X ma takie funkcje, jak: „wykonaj to zamknięcie w wątku w tle”, „... w głównym wątku”, „... w głównym wątku, za 10 sekund”. To sprawia, że ​​wielowątkowość jest banalna .

Połączenia asynchroniczne: tak widział OP. Każde połączenie, które uzyskuje dostęp do Internetu lub cokolwiek innego, co może zająć trochę czasu (np. Odczyt współrzędnych GPS), jest czymś, na co nie można czekać na wynik. Więc masz funkcje, które robią rzeczy w tle, a następnie przechodzisz zamknięcie, aby powiedzieć im, co mają robić, gdy są gotowe.

To mały początek. Pięć sytuacji, w których zamknięcia są rewolucyjne pod względem tworzenia zwartego, czytelnego, niezawodnego i wydajnego kodu.


-4

Zamknięcie to skrótowy sposób napisania metody, w której ma być stosowany. Oszczędza to wysiłku związanego z deklarowaniem i pisaniem osobnej metody. Jest to przydatne, gdy metoda zostanie użyta tylko raz, a jej definicja jest krótka. Korzyści płyną z mniejszego pisania, ponieważ nie ma potrzeby określania nazwy funkcji, jej typu zwracanego ani modyfikatora dostępu. Ponadto podczas czytania kodu nie musisz szukać gdzie indziej definicji metody.

Powyżej znajduje się streszczenie Zrozumienia wyrażeń lambda autorstwa Dana Avidara.

Wyjaśniło mi to użycie zamknięć dla mnie, ponieważ wyjaśnia alternatywy (zamknięcie vs. metoda) i zalety każdego z nich.

Poniższy kod jest używany raz i tylko raz podczas instalacji. Zapisanie go w miejscu w widoku viewDidLoad pozwala zaoszczędzić kłopotów z szukaniem go w innym miejscu i skraca rozmiar kodu.

myPhoton!.getVariable("Temp", completion: { (result:AnyObject!, error:NSError!) -> Void in
  if let e = error {
    self.getTempLabel.text = "Failed reading temp"
  } else {
    if let res = result as? Float {
    self.getTempLabel.text = "Temperature is \(res) degrees"
    }
  }
})

Ponadto zapewnia asynchroniczny proces ukończenia bez blokowania innych części programu, a zamknięcie zachowa wartość do ponownego użycia w kolejnych wywołaniach funkcji.

Kolejne zamknięcie; ten przechwytuje wartość ...

let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)

4
Niestety myli to zamknięcia i lambdas. Jagnięta często używają zamknięć, ponieważ są one zwykle o wiele bardziej przydatne, jeśli są zdefiniowane a) bardzo zwięźle i b) w ramach i w zależności od kontekstu danej metody, w tym zmiennych. Jednak faktyczne zamknięcie nie ma nic wspólnego z ideą lambda, która polega w zasadzie na zdefiniowaniu pierwszej klasy funkcji i przekazaniu jej do późniejszego wykorzystania.
Nathan Tuggy,

Podczas głosowania nad odpowiedzią przeczytaj ją ... Kiedy powinienem głosować w dół? Używaj swoich głosów negatywnych, gdy napotkasz rażąco niechlujny, niewymagający wysiłku post lub odpowiedź, która jest wyraźnie i być może niebezpiecznie niepoprawna. Moja odpowiedź może nie zawierać niektórych powodów użycia zamknięcia, ale nie jest ani rażąco niechlujna, ani niebezpiecznie nieprawidłowa. Istnieje wiele prac i dyskusji na temat zamknięć, które koncentrują się na programowaniu funkcjonalnym i notacji lambda. Cała moja odpowiedź mówi, że to wyjaśnienie programowania funkcjonalnego pomogło mi zrozumieć zamknięcia. To i trwała wartość.
Bendrix,

1
Odpowiedź nie jest może niebezpieczna , ale z pewnością jest „wyraźnie […] niepoprawna”. Używa niewłaściwych terminów dla pojęć, które są wysoce komplementarne, a zatem często używane razem - dokładnie tam, gdzie niezbędna jest jasność rozróżnienia. Może to powodować problemy projektowe dla programistów czytających to, jeśli nie zostaną rozwiązane. (A ponieważ, o ile mogę stwierdzić, przykład kodu po prostu nie zawiera żadnych zamknięć, tylko funkcję lambda, nie pomaga wyjaśnić, dlaczego zamknięcia byłyby pomocne.)
Nathan Tuggy

Nathan, ten przykład kodu jest zamknięciem w Swift. Możesz zapytać kogoś innego, jeśli wątpisz we mnie. Więc jeśli to jest zamknięcie, zagłosowałbyś na nie?
Bendrix,

1
Apple dość jasno opisuje dokładnie, co rozumieją przez zamknięcie w swojej dokumentacji . „Zamknięcia to samodzielne bloki funkcjonalności, które można przekazywać i wykorzystywać w kodzie. Zamknięcia w Swift są podobne do bloków w C i Objective-C oraz do lambdów w innych językach programowania.” Gdy dojdziesz do implementacji i terminologii, może to być mylące .
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.