Buon anno, e bentornati a Ruby Magic! In questo episodio invernale, ci immergeremo in attacchi e scope. Quindi mettetevi gli sci e seguiteci nelle profondità del bosco.
L’ultima volta, abbiamo esaminato le chiusure in Ruby confrontando blocchi, proc e lambda. A parte le differenze tra i tre tipi, abbiamo toccato ciò che definisce una chiusura.
Una chiusura è una funzione di prima classe con un ambiente. L’ambiente è una mappatura delle variabili che esistevano quando la chiusura è stata creata. La chiusura mantiene il suo accesso a queste variabili, anche se sono definite in un altro scope.
Abbiamo esplorato l’equivalente di Ruby alle funzioni di prima classe, ma abbiamo opportunamente saltato gli ambienti. In questo episodio, vedremo come funziona l’ambiente per le chiusure, le classi e le istanze di classe, esaminando come Ruby gestisce lo scope lessicale attraverso i suoi binding.
- Scope lessicale
- Scope ereditati e binding di proc
- Bindings
- Un esempio di vita reale
- Vi abbiamo perso nel bosco? Abbiamo approfondito gli scopi e le chiusure, dopo averli sorvolati. Speriamo di non avervi perso nel bosco. Fateci sapere se volete saperne di più sugli attacchi, o se avete qualsiasi altro argomento Ruby che vorreste approfondire.
Scope lessicale
Nella programmazione, lo scope si riferisce ai binding disponibili in una specifica parte del codice. Un binding, o name binding, lega un nome a un riferimento di memoria, come il nome di una variabile al suo valore. Lo scope definisce il significato self
, i metodi che possono essere chiamati e le variabili che sono disponibili.
Ruby, come la maggior parte dei linguaggi di programmazione moderni, usa uno scope statico, spesso chiamato scope lessicale (in opposizione allo scope dinamico). Lo scope attuale è basato sulla struttura del codice e determina le variabili disponibili in parti specifiche del codice. Questo significa che lo scope cambia quando il codice salta tra i metodi, i blocchi e le classi, dato che possono tutti avere diverse variabili locali, per esempio.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
In questo metodo, creiamo una variabile locale dentro un metodo e la stampiamo alla console. La variabile è nello scope all’interno del metodo, poiché è creata lì.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
In questo esempio, creiamo la variabile fuori dal metodo. Quando chiamiamo la variabile all’interno di un metodo, otteniamo un errore, poiché la variabile è fuori portata. Le variabili locali hanno uno scope stretto, il che significa che un metodo non può accedere a una variabile al di fuori di se stesso a meno che non sia passata come argomento.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Mentre le variabili locali sono disponibili localmente, le variabili di istanza sono disponibili per tutti i metodi di un’istanza di classe.
Scope ereditati e binding di proc
Come abbiamo visto negli esempi precedenti, lo scope è basato sulla posizione nel codice. Una variabile locale definita al di fuori di un metodo non è nello scope all’interno del metodo, ma può essere resa disponibile trasformandola in una variabile di istanza. I metodi non possono accedere alle variabili locali definite al di fuori di loro perché i metodi hanno il loro ambito, con i loro binding.
Le proc (inclusi i blocchi e le lambda, per estensione) sono diverse. Ogni volta che un proc viene istanziato, viene creato un binding che eredita i riferimenti alle variabili locali nel contesto in cui il blocco è stato creato.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
In questo esempio, abbiamo impostato una variabile chiamata foo
su 1
. Internamente, l’oggetto Proc creato sulla seconda linea crea un nuovo binding. Quando chiamiamo il proc, possiamo chiedere il valore della variabile.
Siccome il binding viene creato quando il proc viene inizializzato, non possiamo creare il proc prima di definire la variabile, anche se il blocco non viene chiamato fino a dopo che la variabile è stata definita.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Chiamando il proc si ottiene un NameError
poiché la variabile non è definita nei binding del proc. Quindi, qualsiasi variabile a cui si accede in un proc dovrebbe essere definita prima che il proc sia creato o passato come argomento.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Possiamo, tuttavia, cambiare la variabile dopo che è stata definita nel contesto principale poiché il binding del proc mantiene un riferimento ad essa anziché copiarla.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
In questo esempio, possiamo vedere che la variabile foo
punta allo stesso oggetto sia all’interno che all’esterno del proc. Possiamo aggiornarla all’interno del processo per aggiornare anche la variabile al di fuori di esso.
Bindings
Per tenere traccia dell’ambito corrente, Ruby usa i bindings, che incapsulano il contesto di esecuzione in ogni posizione del codice. Il metodo binding
restituisce un oggetto Binding
che descrive i binding nella posizione corrente.
1 2 |
foo = 1 binding.local_variables # => |
L’oggetto binding ha un metodo chiamato #local_variables
che ritorna i nomi di tutte le variabili locali disponibili nello scope corrente.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Il codice può essere valutato sul binding usando il metodo #eval
. L’esempio sopra non è molto utile, poiché chiamando semplicemente foo
si otterrebbe lo stesso risultato. Tuttavia, poiché un binding è un oggetto che può essere passato in giro, può essere usato per alcune cose più interessanti. Vediamo un esempio.
Un esempio di vita reale
Ora che abbiamo imparato a conoscere i binding nella sicurezza del nostro garage, vogliamo portarli sulle piste e giocare sulla neve. A parte l’uso interno di Ruby dei binding in tutto il linguaggio, ci sono alcune situazioni in cui gli oggetti binding sono usati esplicitamente. Un buon esempio è il sistema di template ERB-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`" |
In questo esempio, creiamo una variabile chiamata x
, un metodo chiamato y
, e un template ERB che fa riferimento ad entrambi. Poi passiamo il binding corrente a ERB#result
, che valuta i tag ERB nel template e restituisce una stringa con le variabili compilate.
Sotto il cappuccio, ERB usa Binding#eval
per valutare il contenuto di ogni tag ERB nell’ambito del binding passato. Un’implementazione semplificata che funziona per l’esempio precedente potrebbe essere come questa:
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`" |
La classe DiyErb
prende una stringa template all’inizializzazione. Il suo metodo #result
trova tutti i tag ERB e li sostituisce con il risultato della valutazione del loro contenuto. Per farlo, chiama Binding#eval
sul binding passato, con il contenuto dei tag ERB.
Passando il binding corrente quando chiama il metodo #result
, le chiamate eval
possono accedere alle variabili definite fuori dal metodo, e anche fuori dalla classe, senza doverle passare esplicitamente.
Vi abbiamo perso nel bosco? Abbiamo approfondito gli scopi e le chiusure, dopo averli sorvolati. Speriamo di non avervi perso nel bosco. Fateci sapere se volete saperne di più sugli attacchi, o se avete qualsiasi altro argomento Ruby che vorreste approfondire.
Grazie per averci seguito e per favore cacciate la neve dai vostri attacchi prima di lasciarli per il prossimo sviluppatore. Se ti piacciono questi viaggi magici, potresti iscriverti a Ruby Magic per ricevere una e-mail quando pubblichiamo un nuovo articolo circa una volta al mese.