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.
Odpowiedzi:
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ęć :
Tutaj
threshold
jest 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.filter
nie 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:
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.
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.
źródło
bestSellingBooks
kod, jak i nafilter
kod, takie jak określony interfejs lub argument danych użytkownika, aby móc komunikować się zthreshold
danymi. To łączy te dwie funkcje razem w znacznie mniej wielokrotny sposób.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:
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ć:
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ę:
Magia, która się tutaj dzieje, polega na tym, że
renderInt
zachowuje dostęp dototal
zmiennej, nawet jeślirenderInt
jest 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
źródło
Celem
closures
jest po prostu zachowanie stanu; stąd nazwaclosure
- zamyka się nad stanem. Dla ułatwienia dalszych wyjaśnień użyję Javascript.Zazwyczaj masz funkcję
gdzie zakres zmiennych jest związany z tą funkcją. Po wykonaniu zmienna
txt
wychodzi 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 .
Prostym, ale co prawda niezbyt przydatnym przykładem jest:
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ęadd5
w wyniku wywołania funkcji wyższego rzędumakeadder
.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 parametra
był5
.Lub pokazać przynajmniej jeden przydatny przykład:
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 jestfunction(){}()
. 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:
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.
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.
źródło
Istnieją dwa główne przypadki użycia dla zamknięć:
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.
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.
źródło
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 strasznierand()
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ływaniarand()
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.
źródło
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:
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.
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):
Tutaj bierzemy oznaczenie funkcji
function
i zwracamy dwa zamknięcia, oba przechwytujące zmienną lokalną o nazwieactive
:function
tylko wtedy, gdyactive
jest to prawdąaction
nanil
akafalse
.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ł:
Należy zauważyć, że zamknięcia dezaktywujące można również nadać również innym funkcjom; tutaj
active
zmienne lokalne nie są współużytkowane przezf
ig
; Ponadto, dodatkowo doactive
,f
odnosi się tylko doobj1
ig
dotyczy tylkoobj2
.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).
źródło
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.Nic jeszcze nie zostało powiedziane, ale może prostszy przykład.
Oto przykład JavaScript wykorzystujący limity czasu:
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,message
co zostało pierwotnie przekazanedelayedLog()
, ponieważ nadal jest dostępne dofire()
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ęcdelayedLog()
zakres nie zniknie, dopóki nie skończy się limit czasu. Ale blokowanie wszystkiego nie jest zbyt przyjemne.Innym sposobem byłoby umieszczenie
message
zmiennej w innym zasięgu, który będzie dostępny podelayedLog()
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
message
zmiennej, gdy jej potrzebujesz. Z tego powodu możesz uciec od pisaniadelayedLog()
jak wyżej.źródło
message
jest objęty zakresem funkcjifire
i dlatego jest opisywany w kolejnych wywołaniach; ale robi to przypadkowo tak powiedzieć. Technicznie jest to zamknięcie. +1 tak czy inaczej;)makeadder
przykł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ę gosetTimeout
.message
wfire
generuje zamknięcie. A kiedy zostanie wywołanysetTimeout
, wykorzystuje stan zachowany.PHP może pomóc w pokazaniu prawdziwego przykładu w innym języku.
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.
źródło
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.
źródło
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.
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ść ...
źródło