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.
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:
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:
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:
Az ECMAScript:
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:
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.
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
- 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