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.