Happy new year, and welcome back to Ruby Magic! I det här vinteravsnittet ska vi dyka ner i bindningar och scopes. Så sätt på dig skidorna och följ med oss djupt in i skogen.
Förra gången tittade vi på closures i Ruby genom att jämföra block, procs och lambdas. Förutom skillnaderna mellan de tre typerna berörde vi vad som definierar en closure.
En closure är en första klassens funktion med en miljö. Miljön är en mappning till de variabler som fanns när closure skapades. Closure behåller sin tillgång till dessa variabler, även om de definieras i ett annat scope.
Vi har utforskat Rubys motsvarighet till förstklassiga funktioner, men vi hoppade bekvämt över miljöer. I det här avsnittet ska vi titta på hur den miljön fungerar för closures, klasser och klassinstanser genom att undersöka hur Ruby hanterar lexikalt omfång genom sina bindningar.
Lexikalt omfång
I programmering hänvisar omfång till de bindningar som är tillgängliga vid en viss del av koden. En bindning, eller namnbindning, binder ett namn till en minnesreferens, som en variabels namn till dess värde. Scopet definierar vad self
betyder, vilka metoder som kan anropas och vilka variabler som är tillgängliga.
Ruby, liksom de flesta moderna programmeringsspråk, använder ett statiskt scope, ofta kallat lexikalt scope (i motsats till dynamiskt scope). Det aktuella scope är baserat på kodens struktur och bestämmer vilka variabler som är tillgängliga vid specifika delar av koden. Detta innebär att räckvidden ändras när koden hoppar mellan metoder, block och klasser – eftersom de alla kan ha olika lokala variabler, till exempel.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
I den här metoden skapar vi en lokal variabel inuti en metod och skriver ut den till konsolen. Variabeln är inom räckvidden inne i metoden, eftersom den skapas där.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
I det här exemplet skapar vi variabeln utanför metoden. När vi anropar variabeln inne i en metod får vi ett fel eftersom variabeln är utanför räckvidden. Lokala variabler har ett snävt scope, vilket innebär att en metod inte kan komma åt en variabel utanför sig själv om den inte skickas som argument.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Men medan lokala variabler är tillgängliga lokalt, är instansvariabler tillgängliga för alla metoder i en klassinstans.
Inherited Scopes and Proc Bindings
Som vi har sett i de tidigare exemplen är räckvidden baserad på platsen i koden. En lokal variabel som definieras utanför en metod är inte inom räckvidden i metoden men kan göras tillgänglig genom att göra den till en instansvariabel. Metoder kan inte få tillgång till lokala variabler som definierats utanför dem eftersom metoder har sitt eget scope, med sina egna bindningar.
Procs (inklusive block och lambdaer, i förlängningen) är annorlunda. När en proc instansieras skapas en bindning som ärver referenser till de lokala variablerna i den kontext som blocket skapades.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
I det här exemplet ställer vi in en variabel som heter foo
till 1
. Internt skapar Proc-objektet som skapas på den andra raden en ny bindning. När vi anropar proc:n kan vi fråga efter värdet på variabeln.
Då bindningen skapas när proc:n initialiseras kan vi inte skapa proc:n innan vi definierar variabeln, även om blocket inte anropas förrän efter att variabeln har definierats.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Ansamlingen av proc:n kommer att ge en NameError
eftersom variabeln inte är definierad i proc:ns bindningar. Därför bör alla variabler som nås i en proc definieras innan proc:n skapas eller skickas som argument.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Vi kan dock ändra variabeln efter att den har definierats i huvudkontexten eftersom proc:ns bindning håller en referens till den istället för att kopiera den.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
I det här exemplet kan vi se att foo
-variabeln pekar på samma objekt när den befinner sig i proc:n som när den befinner sig utanför. Vi kan uppdatera den inne i proc:n för att få variabeln utanför den uppdaterad också.
Bindningar
För att hålla reda på det aktuella tillämpningsområdet använder Ruby bindningar, som kapslar in exekveringskontexten vid varje position i koden. Metoden binding
returnerar ett Binding
-objekt som beskriver bindningarna vid den aktuella positionen.
1 2 |
foo = 1 binding.local_variables # => |
Bindningsobjektet har en metod som heter #local_variables
som returnerar namnen på alla lokala variabler som är tillgängliga i det aktuella scope.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Kod kan utvärderas på bindningen med hjälp av metoden #eval
. Exemplet ovan är inte särskilt användbart, eftersom ett enkelt anrop av foo
skulle ge samma resultat. Men eftersom en bindning är ett objekt som kan skickas runt kan den användas för mer intressanta saker. Låt oss titta på ett exempel.
Ett exempel från det verkliga livet
Nu när vi har lärt oss om bindningar i garagets trygga miljö, kan vi gärna ta med dem ut i backen och leka i snön. Förutom Rubys interna användning av bindningar i hela språket finns det några situationer där bindningsobjekt används explicit. Ett bra exempel är ERB-Rubys mallsystem.
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`" |
I det här exemplet skapar vi en variabel som heter x
, en metod som heter y
och en ERB-mall som refererar till båda. Vi skickar sedan den aktuella bindningen till ERB#result
, som utvärderar ERB-taggarna i mallen och returnerar en sträng med variablerna ifyllda.
Under huven använder ERB Binding#eval
för att utvärdera varje ERB-taggs innehåll inom räckvidden för den överlämnade bindningen. En förenklad implementering som fungerar för exemplet ovan skulle kunna se ut så här:
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`" |
Klassen DiyErb
tar emot en mallsträng vid initialisering. Dess #result
metod hittar alla ERB-taggar och ersätter dem med resultatet av utvärderingen av deras innehåll. För att göra det anropar den Binding#eval
på den överlämnade bindningen, med innehållet i ERB-taggarna.
Genom att överlämna den aktuella bindningen vid anrop av #result
-metoden kan eval
-anropet få tillgång till de variabler som definierats utanför metoden, och till och med utanför klassen, utan att behöva överlämna dem uttryckligen.
Förlorade vi dig i skogen?
Vi hoppas att du tyckte om vår skidresa i skogen. Vi gick djupare in på scopes och closures, efter att ha glosat över dem. Vi hoppas att vi inte har förlorat dig i skogen. Låt oss veta om du vill lära dig mer om bindningar, eller om du har något annat Ruby-ämne som du vill dyka ner i.
Tack för att du följde oss och sparka snön från dina bindningar innan du lämnar dem till nästa utvecklare. Om du gillar dessa magiska resor kanske du vill prenumerera på Ruby Magic för att få ett e-postmeddelande när vi publicerar en ny artikel ungefär en gång i månaden.