Boldog új évet, és üdvözöljük újra a Ruby Magicben! Ebben a téli epizódban a kötésekbe és a hatókörökbe merülünk el. Szóval húzzuk fel a síléceket, és kövessünk minket mélyen az erdőbe.
Legutóbb a lezárásokat vizsgáltuk a Rubyban, összehasonlítva a blokkokat, a procokat és a lambdákat. A három típus közötti különbségeken kívül érintettük, hogy mi határozza meg a zárlatokat.
A zárlat egy első osztályú függvény egy környezettel. A környezet egy leképezés azokra a változókra, amelyek a closure létrehozásakor léteztek. A closure megtartja a hozzáférést ezekhez a változókhoz, még akkor is, ha azok egy másik hatókörben vannak definiálva.
Felfedeztük a Ruby első osztályú függvények megfelelőjét, de kényelmesen átugrottuk a környezeteket. Ebben az epizódban megnézzük, hogyan működik ez a környezet a lezárások, osztályok és osztálypéldányok esetében, megvizsgálva, hogyan kezeli a Ruby a lexikai hatóköröket a kötéseken keresztül.
Lexikai hatókör
A programozásban a hatókör a kód egy adott részén elérhető kötésekre utal. A kötés vagy névkötés egy nevet köt egy memóriahivatkozáshoz, mint ahogy egy változó neve az értékéhez. A hatókör határozza meg, hogy mit self
jelent, milyen metódusok hívhatók, és milyen változók állnak rendelkezésre.
A Ruby, mint a legtöbb modern programozási nyelv, statikus hatókört használ, amelyet gyakran lexikális hatókörnek neveznek (szemben a dinamikus hatókörrel). Az aktuális hatókör a kód szerkezetén alapul, és meghatározza a kód bizonyos részein elérhető változókat. Ez azt jelenti, hogy a hatókör megváltozik, amikor a kód metódusok, blokkok és osztályok között ugrál – mivel ezek mind különböző helyi változókkal rendelkezhetnek például.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
Ezzel a módszerrel létrehozunk egy helyi változót egy metóduson belül, és kiírjuk a konzolra. A változó a metóduson belül hatókörben van, mivel ott hozzuk létre.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
Ebben a példában a változót a metóduson kívül hozzuk létre. Ha a változót a metóduson belül hívjuk meg, hibát kapunk, mivel a változó hatókörön kívül van. A helyi változók szűk hatókörűek, ami azt jelenti, hogy egy metódus nem tud hozzáférni egy önmagán kívül eső változóhoz, hacsak nem adjuk át argumentumként.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Míg a helyi változók lokálisan elérhetők, a példányváltozók egy osztálypéldány minden metódusa számára elérhetők.
Öröklött hatókörök és Proc-kötések
Amint azt az előző példákban láttuk, a hatókör a kódban elfoglalt helytől függ. Egy metóduson kívül definiált helyi változó nincs hatókörben a metóduson belül, de elérhetővé tehető, ha példányváltozóvá alakítjuk. A metódusok nem férhetnek hozzá a rajtuk kívül definiált helyi változókhoz, mert a metódusoknak saját hatókörük van, saját kötöttségekkel.
A folyamatok (beleértve a blokkokat és a lambdákat is, kiterjesztésképpen) másképp vannak. Valahányszor egy procot instanciálunk, létrejön egy kötés, amely örökli a blokk létrehozásának kontextusában lévő helyi változókra való hivatkozásokat.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
A példában a foo
nevű változót 1
-re állítjuk. Belsőleg a második sorban létrehozott Proc objektum új kötést hoz létre. A proc meghívásakor lekérdezhetjük a változó értékét.
Mivel a kötés a proc inicializálásakor jön létre, nem hozhatjuk létre a proc-ot a változó definiálása előtt, még akkor sem, ha a blokkot csak a változó definiálása után hívjuk meg.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
A proc meghívása NameError
eredményez, mivel a változó nincs definiálva a proc kötéseiben. Ezért a proc-ban elért változókat a proc létrehozása vagy argumentumként való átadása előtt definiálni kell.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
A változót azonban megváltoztathatjuk, miután a fő kontextusban definiáltuk, mivel a proc kötése a másolás helyett hivatkozást tart rá.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
Ebben a példában láthatjuk, hogy a foo
változó ugyanarra az objektumra mutat a proc-ban, mint azon kívül. A proc-on belül frissíthetjük, hogy a proc-on kívüli változó is frissüljön.
Megkötések
A Ruby az aktuális hatókör nyomon követésére kötéseket használ, amelyek a kód minden egyes pozíciójában a végrehajtási kontextust kapszulázzák. A binding
metódus egy Binding
objektumot ad vissza, amely az aktuális pozícióban lévő kötéseket írja le.
1 2 |
foo = 1 binding.local_variables # => |
A kötésobjektumnak van egy #local_variables
nevű metódusa, amely az aktuális hatókörben elérhető összes helyi változó nevét adja vissza.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
A kötésen a #eval
metódus segítségével lehet kódot kiértékelni. A fenti példa nem túl hasznos, mivel a foo
egyszerű hívása ugyanezt az eredményt adná. Mivel azonban a kötés egy objektum, amely továbbadható, néhány érdekesebb dologra is felhasználható. Nézzünk egy példát.
Egy példa a való életből
Most, hogy a garázsunk biztonságában megismertük a kötéseket, mintha kivinnénk őket a lejtőkre, és játszanánk a hóban. Eltekintve attól, hogy a Ruby az egész nyelven belül használja a kötéseket, vannak olyan helyzetek, amikor a kötőobjektumokat explicit módon használjuk. Jó példa erre az ERB-Ruby templating rendszere.
1 2 3 4 5 6 7 8 9 10 |
require 'erb' x = 1 def y 2 end template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
Ebben a példában létrehozunk egy x
nevű változót, egy y
nevű metódust és egy ERB-sablont, amely mindkettőre hivatkozik. Ezután átadjuk az aktuális kötést a ERB#result
-nak, amely kiértékeli a sablonban lévő ERB tageket, és egy stringet ad vissza a változókkal kitöltve.
Az ERB a Binding#eval
segítségével kiértékeli az egyes ERB tagek tartalmát az átadott kötés hatókörében. Egy egyszerűsített megvalósítás, amely a fenti példában működik, így nézhet ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class DiyErb def initialize(template) @template = template end def result(binding) @template.gsub(/<%=(.+?)%>/) do binding.eval() end end end x = 1 def y 2 end template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
A DiyErb
osztály inicializáláskor egy sablonsztringet vesz át. A #result
metódusa megkeresi az összes ERB taget, és a tartalmuk kiértékelésének eredményével helyettesíti őket. Ehhez az átadott kötésen meghívja a Binding#eval
-t, az ERB címkék tartalmával.
Azzal, hogy a #result
metódus hívásakor átadja az aktuális kötést, a eval
hívások hozzáférhetnek a metóduson kívül, sőt az osztályon kívül definiált változókhoz is, anélkül, hogy azokat explicit módon át kellene adnunk.
Elvesztettük az erdőben?
Reméljük, jól érezték magukat az erdei sítúránkon. Elmélyedtünk a hatókörökben és a lezárásokban, miután szépítettük őket. Reméljük, nem veszítettünk el titeket az erdőben. Szólj nekünk, ha többet szeretnél megtudni a kötésekről, vagy bármilyen más Ruby témában szeretnél elmerülni.
Köszönjük, hogy követtél minket, és kérlek, rúgd le a havat a kötéseidről, mielőtt elhagyod őket a következő fejlesztőnek. Ha tetszenek ezek a varázslatos utazások, akkor érdemes feliratkoznod a Ruby Magicre, hogy e-mailben értesülj, ha körülbelül havonta egyszer új cikket teszünk közzé.