Anul nou fericit și bine ați revenit la Ruby Magic! În acest episod de iarnă, ne vom scufunda în bindings și scopes. Așa că puneți-vă schiurile și urmați-ne în adâncul pădurii.
Din ultima dată, am analizat închiderile în Ruby comparând blocurile, procs și lambdas. În afară de diferențele dintre cele trei tipuri, am abordat ceea ce definește o închidere.
O închidere este o funcție de primă clasă cu un mediu. Mediul este o cartografiere a variabilelor care existau atunci când a fost creată închiderea. Închiderea își păstrează accesul la aceste variabile, chiar dacă acestea sunt definite într-un alt domeniu.
Am explorat echivalentul lui Ruby pentru funcțiile de primă clasă, dar am sărit în mod convenabil peste medii. În acest episod, vom vedea cum funcționează acel mediu pentru închideri, clase și instanțe de clasă, examinând modul în care Ruby gestionează domeniul de aplicare lexical prin intermediul legăturilor sale.
Domeniul de aplicare lexical
În programare, domeniul de aplicare se referă la legăturile disponibile într-o anumită parte a codului. Un binding, sau name binding, leagă un nume de o referință de memorie, cum ar fi numele unei variabile de valoarea sa. Domeniul de cuprindere definește ceea ce self
înseamnă, metodele care pot fi apelate și variabilele care sunt disponibile.
Ruby, ca majoritatea limbajelor de programare moderne, utilizează un domeniu de cuprindere static, adesea numit domeniu de cuprindere lexical (spre deosebire de domeniul de cuprindere dinamic). Sfera curentă se bazează pe structura codului și determină variabilele disponibile în anumite părți ale codului. Acest lucru înseamnă că domeniul de cuprindere se schimbă atunci când codul sare între metode, blocuri și clase – deoarece toate acestea pot avea variabile locale diferite, de exemplu.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
În această metodă, creăm o variabilă locală în interiorul unei metode și o imprimăm în consolă. Variabila este în domeniul de aplicare în interiorul metodei, deoarece este creată acolo.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
În acest exemplu, creăm variabila în afara metodei. Atunci când apelăm variabila în interiorul unei metode, primim o eroare, deoarece variabila este în afara domeniului de aplicare. Variabilele locale au o sferă de cuprindere restrânsă, ceea ce înseamnă că o metodă nu poate accesa o variabilă din afara sa decât dacă aceasta este trecută ca argument.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
În timp ce variabilele locale sunt disponibile la nivel local, variabilele de instanță sunt disponibile pentru toate metodele unei instanțe de clasă.
Scopuri moștenite și legăturile cu proc
După cum am văzut în exemplele anterioare, domeniul de aplicare se bazează pe locația în cod. O variabilă locală definită în afara unei metode nu se află în sfera de cuprindere în interiorul metodei, dar poate fi pusă la dispoziție prin transformarea ei într-o variabilă de instanță. Metodele nu pot accesa variabile locale definite în afara lor, deoarece metodele au propriul domeniu de aplicare, cu propriile lor legături.
Procurile (inclusiv blocurile și lambda-urile, prin extensie) sunt diferite. Ori de câte ori un proc este instanțiat, se creează un binding care moștenește referințele la variabilele locale din contextul în care blocul a fost creat.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
În acest exemplu, am setat o variabilă numită foo
la 1
. La nivel intern, obiectul Proc creat pe a doua linie creează o nouă legătură. Atunci când apelăm proc, putem cere valoarea variabilei.
Din moment ce legătura este creată atunci când proc este inițializat, nu putem crea proc înainte de a defini variabila, chiar dacă blocul nu este apelat decât după ce variabila este definită.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Invocarea proc-ului va produce un NameError
, deoarece variabila nu este definită în legăturile proc-ului. Astfel, orice variabilă accesată într-un proc ar trebui să fie definită înainte ca proc-ul să fie creat sau să fie trecută ca argument.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Potem, totuși, să modificăm variabila după ce a fost definită în contextul principal, deoarece legarea proc-ului deține o referință la ea în loc să o copiem.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
În acest exemplu, putem vedea că variabila foo
indică același obiect atunci când se află în proc ca și în afara lui. O putem actualiza în interiorul proc pentru ca și variabila din afara lui să fie actualizată.
Bindings
Pentru a ține evidența domeniului curent, Ruby folosește bindings, care încapsulează contextul de execuție în fiecare poziție din cod. Metoda binding
returnează un obiect Binding
care descrie bindings la poziția curentă.
1 2 |
foo = 1 binding.local_variables # => |
Obiectul binding are o metodă numită #local_variables
care returnează numele tuturor variabilelor locale disponibile în domeniul curent.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Codul poate fi evaluat pe binding prin utilizarea metodei #eval
. Exemplul de mai sus nu este foarte util, deoarece simpla apelare a foo
ar avea același rezultat. Cu toate acestea, din moment ce o legătură este un obiect care poate fi transmis, poate fi utilizată pentru lucruri mai interesante. Să ne uităm la un exemplu.
Un exemplu din viața reală
Acum că am învățat despre bindings în siguranța garajului nostru, cum ar fi să le scoatem pe pârtii și să ne jucăm în zăpadă. În afară de utilizarea internă de către Ruby a legăturilor în tot limbajul, există unele situații în care obiectele de legare sunt utilizate în mod explicit. Un bun exemplu este ERB-Sistemul de șabloane al lui Ruby.
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`" |
În acest exemplu, creăm o variabilă numită x
, o metodă numită y
și un șablon ERB care face referire la ambele. Apoi transmitem legătura curentă către ERB#result
, care evaluează etichetele ERB din șablon și returnează un șir de caractere cu variabilele completate.
Sub capotă, ERB folosește Binding#eval
pentru a evalua conținutul fiecărei etichete ERB în domeniul de aplicare al legăturii transmise. O implementare simplificată care funcționează pentru exemplul de mai sus ar putea arăta astfel:
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`" |
Clasa DiyErb
primește un șir de șabloane la inițializare. Metoda sa #result
găsește toate etichetele ERB și le înlocuiește cu rezultatul evaluării conținutului lor. Pentru a face acest lucru, ea apelează Binding#eval
pe legătura transmisă, cu conținutul etichetelor ERB.
Prin transmiterea legăturii curente la apelarea metodei #result
, apelurile eval
pot accesa variabilele definite în afara metodei, și chiar în afara clasei, fără a fi nevoie să le transmită în mod explicit.
V-am pierdut în pădure?
Sperăm că v-ați bucurat de excursia noastră cu schiurile în pădure. Am aprofundat domeniile de aplicare și închiderile, după ce le-am trecut cu vederea. Sperăm că nu v-am pierdut în pădure. Vă rugăm să ne anunțați dacă doriți să aflați mai multe despre legături sau dacă aveți orice alt subiect Ruby în care ați dori să vă scufundați.
Mulțumim că ne-ați urmărit și vă rugăm să dați cu piciorul zăpezii de pe legăturile dumneavoastră înainte de a le lăsa pentru următorul dezvoltator. Dacă vă plac aceste călătorii magice, ați putea dori să vă abonați la Ruby Magic pentru a primi un e-mail atunci când publicăm un nou articol aproximativ o dată pe lună.
.