Lezárás (closure) a programozásban — definíció, működés és példák

Lezárás (closure) a programozásban: definíció, működés és gyakorlati példák különböző nyelveken — könnyen érthető magyarázat és szemléltető kódrészletek.

Szerző: Leandro Alegsa

A számítástechnikában a lezárás (angolul: closure) egy olyan függvény, amely saját környezettel rendelkezik. Ebben a környezetben van legalább egy kötött változó (egy név, amelynek értéke van, például egy szám). A lezárás környezete a kötött változókat a lezárás használata között a memóriában tartja, így a belső függvény a külső függvény lefutása után is eléri ezeket az értékeket.

Rövid történet

Peter J. Landin 1964-ben adta ennek az ötletnek a lezárás nevet. A Scheme programozási nyelv 1975 után tette népszerűvé a lezárásokat. Azóta sok modern programozási nyelv (például JavaScript, Python, Ruby, C++11-től kezdve lambda kifejezések) támogatja a lezárás-szerű mechanizmust.

Mi különbözteti meg a lezárást egy egyszerű függvénytől?

  • Környezeti állapot: a lezárás nemcsak a függvény törzséből áll, hanem a függvényhez tartozó környezetből (a külső változók értékei vagy referenciái).
  • Élettartam: a lezárás által hivatkozott változók élettartama meghosszabbodik a lezárás életéig; nem kerülnek automatikusan felszabadításra a külső függvény befejeződése után.
  • Nevek és anonim függvények: a névtelen függvények (azaz név nélküli függvények) gyakran lezárások lehetnek, de önmagukban a névtelenség nem teszi őket lezárássá. Egy névtelen függvény akkor tekinthető lezárásnak, ha van saját környezete legalább egy kötött változóval. Egy névvel ellátott függvény ugyanakkor lehet lezárás is, ha bezárt környezettel rendelkezik.

Hogyan működik (lexikális vs. dinamikus hatókör)

A legtöbb modern nyelv lexikális (statikus) hatókört használ: egy belső függvény a szövegkörnyezetében (forráskódban) lévő változókat köti meg. Ez azt jelenti, hogy a lezárás a definíció helyén lévő környezetre hivatkozik, nem pedig arra, ahonnan futtatják. Egyes nyelvek (ritkábban) dinamikus hatókört használnak, ahol a futási környezet a meghatározó.

Példák

JavaScript (tipikus számláló példa):

function makeCounter() {   let count = 0;   return function() {     count += 1;     return count;   }; } const c = makeCounter(); console.log(c()); // 1 console.log(c()); // 2

Magyarázat: a belső anonim függvény hozzáfér a külső makeCounter() scope-jában lévő count változóhoz, és a változó értéke megmarad a hívások között.

JavaScript — gyakori buktató ciklusokkal (var vs let):

for (var i = 0; i < 3; i++) {   setTimeout(function() { console.log(i); }, 0); // mindhárom 3-at ír ki } for (let j = 0; j < 3; j++) {   setTimeout(function() { console.log(j); }, 0); // 0,1,2 — minden iterációnak saját j értéke van }

Magyarázat: a var funkciószintű, így az összes lezárás ugyanazt a változót látja; a let blokk-szintű, ezért minden iteráción saját kötés jön létre.

Python:

def make_multiplier(factor):     def multiply(x):         return x * factor     return multiply  double = make_multiplier(2) print(double(5))  # 10

Megjegyzés: Pythonban, ha a belső függvény meg akarja változtatni a külső változót, használni kell a nonlocal (vagy globális esetén a global) kulcsszót.

Scheme (rövid példa a lezárás természetére):

(define (make-counter)   (let ((count 0))     (lambda ()       (set! count (+ count 1))       count)))

Nyelvi különbségek és megvalósítási részletek

  • Bizonyos nyelvek a lezárásoknál a változókat referenciaként tárolják (a futó környezettel kötnek össze), mások például C++-ban lehetővé teszik explicit érték- vagy referenciaalapú capture-t (capture by value vagy by reference).
  • A memóriakezelés: a szemétgyűjtővel rendelkező nyelvek (pl. JavaScript, Python) általában egyszerűbbé teszik, mert a lezárás által használt objektumok addig élnek, amíg a lezárás elérhető. Manuális memóriakezelésnél figyelni kell, nehogy élettartam problémákhoz vezessen.

Tipikus felhasználások

  • Adatelrejtés / enkapszuláció: belső állapot rejtése a külső kód elől (pl. privát változók előállítása).
  • Callback-ok és eseménykezelés: állapotot megőrző kezelők létrehozása.
  • Funkcionális programozási technikák: currying, részleges alkalmazás, magasabb rendű függvények létrehozása.

Gyakori buktatók

  • Változók megosztása és váratlan értékváltozások (különösen ciklusokban, ha nem megfelelő a hatókör kezelése).
  • Memóriaszivárgásra hajlamos helyzetek: ha lezárások sok és hosszú életű objektumokat tartanak élve feleslegesen.
  • Teljesítmény: túl sok kis lezárás létrehozása és tartása növelheti a memória- és futásidő-költséget.

Összefoglalás

A lezárás egy erős eszköz a programozásban: lehetővé teszi, hogy egy függvény megőrizze a környezetét és a hozzá tartozó állapotot a későbbi hívásokhoz. Ez sok hasznos mintát tesz lehetővé (privát állapot, currying, callback-ek), ugyanakkor figyelmet igényel a hatókör, a változó-illesztés (capture) módja és az élettartam kezelés miatt.

A névtelen függvényeket (név nélküli függvények) néha tévesen lezárásoknak nevezik. A legtöbb olyan nyelv, amely névtelen függvényekkel rendelkezik, rendelkezik lezárásokkal is. Egy névtelen függvény akkor is lezárás, ha van saját környezete legalább egy kötött változóval. Egy névtelen függvény, amelynek nincs saját környezete, nem lezárás. Egy névvel ellátott lezárás nem névtelen.

Lezárások és első osztályú függvények

Az értékek lehetnek számok vagy más típusú adatok, például betűk, vagy egyszerűbb részekből álló adatszerkezetek. Egy programozási nyelv szabályai szerint az első osztályú értékek olyan értékek, amelyeket függvényeknek adhatunk meg, függvények adnak vissza, és változó névhez köthetők. Az olyan függvényeket, amelyek más függvényeket vesznek át vagy adnak vissza, magasabb rendű függvényeknek nevezzük. A legtöbb olyan nyelvben, ahol a függvények első osztályú értékek, vannak magasabb rendű függvények és lezárások is.

Nézzük meg például a következő Scheme függvényt:

; Visszaadja az összes olyan könyv listáját, amelyből legalább THRESHOLD példányt eladtak. (define (best-selling-books threshold) (filter (lambda (book) (>= (book-sales book) threshold)) book-list))

Ebben a példában a lambda kifejezés (lambda (könyv) (>= (könyv-értékesítés könyv) küszöbérték))) a legkelendőbb könyvek függvény része. A függvény futtatásakor a Scheme-nek a lambda értékét kell megtennie. Ezt úgy teszi, hogy létrehoz egy lezárást a lambda kódjával és egy hivatkozással a threshold változóra, amely egy szabad változó a lambdán belül. (A szabad változó egy olyan név, amely nincs értékhez kötve).

A szűrőfüggvény ezután lefuttatja a lezárást a listában szereplő minden egyes könyvre, hogy kiválassza, mely könyveket adja vissza. Mivel maga a zárlat rendelkezik a küszöbértékre való hivatkozással, a zárlat minden alkalommal használhatja ezt az értéket, amikor a filter futtatja a zárlatot. Maga a filter függvényt egy teljesen különálló fájlban lehet megírni.

Itt van ugyanez a példa átírva ECMAScript (JavaScript) nyelven, egy másik népszerű nyelven, amely támogatja a lezárásokat:

// Visszaadja az összes olyan könyv listáját, amelyből legalább a "küszöbérték" eladott példányszámot elérte. function bestSellingBooks(threshold) { return bookList. filter( function(book) { return book. sales >= threshold; }     ); }

Az ECMAScript itt a lambda helyett a function szót használja, a filter függvény helyett pedig az Array.filter metódust, de egyébként a kód ugyanazt a dolgot teszi ugyanúgy.

Egy függvény létrehozhat egy lezárást és visszaadhatja azt. A következő példa egy függvény, amely egy függvényt ad vissza.

A rendszerben:

; Adjon vissza egy függvényt, amely megközelíti az f deriváltját ; egy dx intervallum segítségével, amelynek megfelelően kicsinek kell lennie. (define (derivative f dx) (lambda (x) (/ (- (f (+ x dx))) (f x)) dx)))))

Az ECMAScript:

// Adjon vissza egy függvényt, amely megközelíti az f deriváltját // egy dx intervallummal, amelynek megfelelően kicsinek kell lennie. function derivative(f, dx) { return function(x) { return (f(x + dx) - f(x)) / dx; }; }; }

A zárókörnyezet megtartja az f és dx kötött változókat, miután a záró függvény (derivált) visszatér. A zártság nélküli nyelvekben ezek az értékek elvesznének a záró függvény visszatérése után. A zártsággal rendelkező nyelvekben a kötött változót mindaddig a memóriában kell tartani, amíg bármelyik zártság rendelkezik vele.

A lezárást nem kell névtelen függvényt használni. A Python programozási nyelv például csak korlátozottan támogatja az anonim függvényeket, de rendelkezik lezárásokkal. A fenti ECMAScript példa például a következő módon valósítható meg Pythonban:

# Adjon vissza egy függvényt, amely megközelíti az f # deriváltját egy dx intervallummal, amelynek megfelelően kicsinek kell lennie. def derivative(f, dx): def gradient(x): return (f(x + dx) - f(x)) / dx return gradiens

Ebben a példában a gradiens nevű függvény az f és dx változókkal együtt zárja le a függvényt. A külső, derivált nevű függvény ezt a zárlatot adja vissza. Ebben az esetben egy névtelen függvény is működne.

def derivative(f, dx): return lambda x: (f(x + dx) - f(x)) / dx

A Pythonban gyakran megnevezett függvényeket kell használni, mert a lambda-kifejezések csak más kifejezéseket (értéket visszaadó kódot) tartalmazhatnak, utasításokat (olyan kódot, amelynek hatása van, de nincs értéke) nem. Más nyelvekben, például a Scheme-ben azonban minden kód értéket ad vissza; a Scheme-ben minden kifejezés.

Zárások használata

A lezárásoknak számos felhasználási módja van:

  • A szoftverkönyvtárak tervezői lehetővé tehetik a felhasználók számára a viselkedés testreszabását azáltal, hogy lezárásokat adnak át argumentumként a fontos függvényeknek. Például egy értékeket rendező függvény elfogadhat egy olyan záró argumentumot, amely a rendezni kívánt értékeket egy felhasználó által meghatározott kritérium szerint hasonlítja össze.
  • Mivel a lezárások késleltetik a kiértékelést - azaz nem "csinálnak" semmit, amíg meg nem hívják őket -, vezérlési struktúrák definiálására használhatók. Például a Smalltalk összes szabványos vezérlési struktúrája, beleértve az elágazásokat (if/then/else) és a ciklusokat (while és for), olyan objektumok segítségével definiálható, amelyek metódusai elfogadják a lezárásokat. A felhasználók könnyen definiálhatják saját vezérlési struktúráikat is.
  • Több olyan függvény is előállítható, amelyek ugyanazt a környezetet zárják össze, lehetővé téve számukra a privát kommunikációt a környezet megváltoztatásával (olyan nyelveken, amelyek lehetővé teszik a hozzárendelést).

A rendszerben

(define foo #f) (define bar #f) (let ((secret-message "none")) (set! foo (lambda (msg) (set! secret-message msg)))) (set! bar (lambda () secret-message)))) (display (bar)) ; nyomtat "none" (newline) (foo "meet me by the docks at midnight") (display (bar)) ; nyomtat "meet me by the docks at midnight"
  • A lezárások objektumrendszerek megvalósítására használhatók.

Megjegyzés: Egyes beszélők minden olyan adatszerkezetet, amely lexikális környezetet köt le, lezárásnak neveznek, de ez a kifejezés általában kifejezetten a függvényekre vonatkozik.

Kérdések és válaszok

K: Mi az a zárás az informatikában?


V: A zárlat egy olyan függvény, amely saját környezettel rendelkezik.

K: Mit tartalmaz egy zárlat környezete?


V: Egy zárlat környezete legalább egy kötött változót tartalmaz.

K: Ki adta a closure ötletének a nevét?


V: Peter J. Landin 1964-ben adta a closure ötletének a nevét.

K: Melyik programozási nyelv tette népszerűvé a lezárásokat 1975 után?


V: A Scheme programozási nyelv tette népszerűvé a lezárásokat 1975 után.

K: Az anonim függvények és a lezárások ugyanazok?


V: A névtelen függvényeket néha tévesen lezárásoknak nevezik, de nem minden névtelen függvény lezárás.

K: Mitől lesz egy névtelen függvény zárlat?


V: Egy névtelen függvény akkor zárlat, ha van saját környezete legalább egy kötött változóval.

K: Egy névvel ellátott zárlat névtelen?


V: Nem, a megnevezett zárlat nem névtelen.


Keres
AlegsaOnline.com - 2020 / 2025 - License CC3