Hyvää uutta vuotta ja tervetuloa takaisin Ruby Magiciin! Tässä talven jaksossa sukellamme sidoksiin ja laajuuksiin. Laita siis sukset jalkaan ja seuraa meitä syvälle metsään.
Viime kerralla tarkastelimme sulkuja Rubyssä vertailemalla blockeja, proceja ja lambdoja. Kolmen tyypin välisten erojen lisäksi käsittelimme sitä, mikä määrittelee sulkeuman.
Sulkeuma on ensimmäisen luokan funktio, jolla on ympäristö. Ympäristö on kartoitus muuttujiin, jotka olivat olemassa, kun closure luotiin. Sulkeuma säilyttää pääsyn näihin muuttujiin, vaikka ne olisivatkin määritelty toisessa laajuudessa.
Olemme tutkineet Rubyn vastinetta ensimmäisen luokan funktioille, mutta ohitimme kätevästi ympäristöt. Tässä jaksossa tarkastelemme, miten tuo ympäristö toimii sulkujen, luokkien ja luokkainstanssien kohdalla tutkimalla, miten Ruby käsittelee leksikaalista laajuutta sidonnaisuuksiensa avulla.
Leksikaalinen laajuus
Ohjelmoinnissa laajuudella tarkoitetaan tietyssä koodin osassa käytettävissä olevia sidonnaisuuksia. Sidonta eli nimisidonta sitoo nimen muistiviittaukseen, kuten muuttujan nimi sen arvoon. Laajuus määrittelee, mitä self
tarkoittaa, mitä metodeja voidaan kutsua ja mitkä muuttujat ovat käytettävissä.
Ruby, kuten useimmat nykyaikaiset ohjelmointikielet, käyttää staattista laajuutta, jota kutsutaan usein leksikaaliseksi laajuudeksi (toisin kuin dynaamista laajuutta). Nykyinen laajuus perustuu koodin rakenteeseen ja määrittää koodin tietyissä kohdissa käytettävissä olevat muuttujat. Tämä tarkoittaa, että laajuus muuttuu, kun koodissa hypitään metodien, lohkojen ja luokkien välillä – sillä niissä kaikissa voi olla esimerkiksi erilaisia paikallisia muuttujia.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
Tässä metodissa luodaan metodin sisälle paikallinen muuttuja ja tulostetaan se konsoliin. Muuttuja on metodin sisällä laajuudessa, koska se on luotu sinne.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
Tässä esimerkissä luomme muuttujan metodin ulkopuolelle. Kun kutsumme muuttujaa metodin sisällä, saamme virheilmoituksen, koska muuttuja on alueen ulkopuolella. Paikalliset muuttujat ovat tiukasti rajattuja, eli metodi ei voi käyttää muuttujaa itsensä ulkopuolella, ellei sitä välitetä argumenttina.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Vaikka paikalliset muuttujat ovat käytettävissä paikallisesti, niin instanssimuuttujat ovat luokan instanssin kaikkien metodien käytettävissä.
Perinneet laajuudet ja Proc-sidonnat
Kuten edellisissä esimerkeissä nähtiin, laajuus perustuu sijaintiin koodissa. Metodin ulkopuolella määritelty paikallinen muuttuja ei ole metodin sisällä laajuudessa, mutta se voidaan saada käyttöön muuttamalla se instanssimuuttujaksi. Metodit eivät pääse käsiksi niiden ulkopuolella määriteltyihin paikallisiin muuttujiin, koska metodeilla on oma scope, jolla on omat sidonnaisuutensa.
Protokollat (mukaan lukien lohkot ja lambdat, laajennettuna) ovat erilaisia. Aina kun proc instantioidaan, luodaan sidonta, joka perii viittaukset paikallisiin muuttujiin siinä kontekstissa, jossa lohko luotiin.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
Tässä esimerkissä asetamme muuttujan nimeltä foo
arvoksi 1
. Sisäisesti toisella rivillä luotu Proc-olio luo uuden sidoksen. Kutsuessamme proc:ia voimme kysyä muuttujan arvoa.
Koska sidonta luodaan, kun proc:ia alustetaan, emme voi luoda proc:ia ennen muuttujan määrittelyä, vaikka lohkoa kutsutaan vasta muuttujan määrittelyn jälkeen.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Procin kutsuminen tuottaa NameError
, koska muuttujaa ei ole määritelty procin sidonnoissa. Näin ollen kaikki muuttujat, joita käytetään procissa, tulisi määritellä ennen kuin proc luodaan tai annetaan argumenttina.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Voidaan kuitenkin muuttaa muuttujaa sen jälkeen, kun se on määritetty pääkontekstissa, koska procin sidonnaisuudet säilyttävät viittauksen siihen kopioinnin sijaan.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
Tässä esimerkissä näemme, että muuttuja foo
osoittaa samaan objektiin procissa ollessaan kuin sen ulkopuolella. Voimme päivittää sen proc:n sisällä, jolloin myös sen ulkopuolella oleva muuttuja päivittyy.
Sidokset
Tämänhetkisen laajuuden seuraamiseksi Ruby käyttää sidoksia, jotka kapseloivat suorituskontekstin kussakin koodin kohdassa. Metodi binding
palauttaa Binding
-olion, joka kuvaa sidonnat nykyisessä kohdassa.
1 2 |
foo = 1 binding.local_variables # => |
Sidonta-oliolla on metodi nimeltä #local_variables
, joka palauttaa kaikkien nykyisessä scope-alueessa käytettävissä olevien paikallismuuttujien nimet.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Koodia voidaan arvioida sitovassa objektissa metodilla #eval
. Yllä oleva esimerkki ei ole kovin hyödyllinen, sillä pelkkä foo
:n kutsuminen johtaisi samaan tulokseen. Koska sidonta on kuitenkin objekti, jota voidaan siirtää, sitä voidaan käyttää mielenkiintoisempiin asioihin. Katsotaanpa yksi esimerkki.
Todellisen elämän esimerkki
Nyt kun olemme oppineet sidonnoista autotallimme turvallisessa tilassa, haluamme viedä ne ulos rinteisiin ja leikkiä lumessa. Sen lisäksi, että Ruby käyttää sidoksia sisäisesti koko kielessä, on joitakin tilanteita, joissa sidontaobjekteja käytetään eksplisiittisesti. Hyvä esimerkki on ERB-Rubyn templating-järjestelmä.
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`" |
Tässä esimerkissä luomme muuttujan nimeltä x
, metodin nimeltä y
ja ERB-mallin, joka viittaa molempiin. Sitten välitämme nykyisen sidoksen ERB#result
:lle, joka arvioi mallin ERB-tagit ja palauttaa merkkijonon, jossa muuttujat on täytetty.
Hupun alla ERB käyttää Binding#eval
:tä arvioimaan jokaisen ERB-tagin sisällön välitetyn sidoksen vaikutusalueella. Yksinkertaistettu toteutus, joka toimii yllä olevassa esimerkissä, voisi näyttää seuraavalta:
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`" |
Luokka DiyErb
ottaa mallin merkkijonon alustuksen yhteydessä. Sen #result
-metodi etsii kaikki ERB-tunnisteet ja korvaa ne niiden sisällön arvioinnin tuloksella. Tätä varten se kutsuu Binding#eval
välitetyllä sidonnalla ERB-tunnisteiden sisällön.
Välittämällä nykyisen sidonnan #result
-metodia kutsuttaessa eval
-kutsut pääsevät käsiksi metodin ja jopa luokan ulkopuolella määriteltyihin muuttujiin ilman, että niitä tarvitsee välittää eksplisiittisesti.
Kadotimmeko teidät metsään?
Toivomme, että nautitte hiihtomatkastamme metsään. Tutustuimme syvällisemmin katvealueisiin ja sulkuihin, kun olimme kaunistelleet niitä. Toivottavasti emme hukanneet teitä metsään. Kerro meille, jos haluat oppia lisää sidonnoista tai sinulla on jokin muu Ruby-aihe, johon haluaisit sukeltaa.
Kiitos, että seurasit meitä ja potkaise lumi pois sidonnoistasi ennen kuin jätät ne seuraavalle kehittäjälle. Jos pidät näistä maagisista matkoista, saatat haluta tilata Ruby Magicin saadaksesi sähköpostia, kun julkaisemme uuden artikkelin noin kerran kuukaudessa.