Happy new year, and welcome back to Ruby Magic! In dieser Winterepisode werden wir uns mit Bindungen und Scopes beschäftigen. Also zieh deine Skier an und folge uns tief in den Wald.
Letztes Mal haben wir uns mit Closures in Ruby beschäftigt, indem wir Blöcke, Procs und Lambdas verglichen haben. Abgesehen von den Unterschieden zwischen den drei Typen haben wir uns damit beschäftigt, was eine Closure definiert.
Eine Closure ist eine Funktion erster Klasse mit einer Umgebung. Die Umgebung ist eine Zuordnung zu den Variablen, die existierten, als die Closure erstellt wurde. Die Closure behält den Zugriff auf diese Variablen, auch wenn sie in einem anderen Bereich definiert sind.
Wir haben Rubys Äquivalent zu Funktionen erster Klasse erforscht, aber wir haben bequemerweise die Umgebungen übersprungen. In dieser Folge sehen wir uns an, wie diese Umgebung für Closures, Klassen und Klasseninstanzen funktioniert, indem wir untersuchen, wie Ruby den lexikalischen Bereich durch seine Bindungen handhabt.
Lexikalischer Bereich
In der Programmierung bezieht sich der Bereich auf die Bindungen, die an einem bestimmten Teil des Codes verfügbar sind. Eine Bindung, oder Namensbindung, bindet einen Namen an eine Speicherreferenz, wie der Name einer Variablen an ihren Wert. Der Gültigkeitsbereich definiert, was self
bedeutet, welche Methoden aufgerufen werden können und welche Variablen verfügbar sind.
Ruby verwendet, wie die meisten modernen Programmiersprachen, einen statischen Gültigkeitsbereich, oft lexikalischer Gültigkeitsbereich genannt (im Gegensatz zum dynamischen Gültigkeitsbereich). Der aktuelle Bereich basiert auf der Struktur des Codes und bestimmt die Variablen, die an bestimmten Stellen des Codes verfügbar sind. Das bedeutet, dass sich der Gültigkeitsbereich ändert, wenn der Code zwischen Methoden, Blöcken und Klassen springt, da diese zum Beispiel alle unterschiedliche lokale Variablen haben können.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
In dieser Methode wird eine lokale Variable innerhalb einer Methode erstellt und auf der Konsole ausgegeben. Die Variable ist innerhalb der Methode im Gültigkeitsbereich, da sie dort erstellt wird.
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 diesem Beispiel erstellen wir die Variable außerhalb der Methode. Wenn wir die Variable innerhalb einer Methode aufrufen, erhalten wir einen Fehler, da die Variable außerhalb des Gültigkeitsbereichs liegt. Lokale Variablen sind eng begrenzt, was bedeutet, dass eine Methode nicht auf eine Variable außerhalb ihrer selbst zugreifen kann, es sei denn, sie wird als Argument übergeben.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Während lokale Variablen lokal verfügbar sind, sind Instanzvariablen für alle Methoden einer Klasseninstanz verfügbar.
Vererbte Geltungsbereiche und Proc-Bindungen
Wie wir in den vorherigen Beispielen gesehen haben, basiert der Geltungsbereich auf der Position im Code. Eine lokale Variable, die außerhalb einer Methode definiert ist, hat innerhalb der Methode keinen Geltungsbereich, kann aber verfügbar gemacht werden, indem sie in eine Instanzvariable umgewandelt wird. Methoden können nicht auf lokale Variablen zugreifen, die außerhalb von ihnen definiert sind, weil Methoden ihren eigenen Gültigkeitsbereich mit ihren eigenen Bindungen haben.
Procs (einschließlich Blöcke und Lambdas, durch Erweiterung) sind anders. Wann immer ein Proc instanziiert wird, wird eine Bindung erstellt, die Referenzen auf die lokalen Variablen in dem Kontext erbt, in dem der Block erstellt wurde.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
In diesem Beispiel setzen wir eine Variable namens foo
auf 1
. Intern erzeugt das in der zweiten Zeile erstellte Proc-Objekt eine neue Bindung. Beim Aufruf der Proc können wir den Wert der Variablen abfragen.
Da die Bindung bei der Initialisierung der Proc erstellt wird, können wir die Proc nicht erstellen, bevor wir die Variable definiert haben, auch wenn der Block erst aufgerufen wird, nachdem die Variable definiert wurde.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Der Aufruf der proc erzeugt ein NameError
, da die Variable nicht in den Bindungen der proc definiert ist. Daher sollten alle Variablen, auf die in einer Proc zugegriffen wird, definiert werden, bevor die Proc erstellt oder als Argument übergeben wird.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Wir können jedoch die Variable ändern, nachdem sie im Hauptkontext definiert wurde, da die Bindung der Proc einen Verweis auf sie enthält, anstatt sie zu kopieren.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
In diesem Beispiel können wir sehen, dass die Variable foo
innerhalb der Proc auf dasselbe Objekt zeigt wie außerhalb. Wir können sie innerhalb der Prozedur aktualisieren, damit die Variable außerhalb der Prozedur ebenfalls aktualisiert wird.
Bindings
Um den aktuellen Gültigkeitsbereich zu verfolgen, verwendet Ruby Bindings, die den Ausführungskontext an jeder Position im Code kapseln. Die binding
-Methode gibt ein Binding
-Objekt zurück, das die Bindungen an der aktuellen Position beschreibt.
1 2 |
foo = 1 binding.local_variables # => |
Das Bindungsobjekt hat eine Methode namens #local_variables
, die die Namen aller im aktuellen Bereich verfügbaren lokalen Variablen zurückgibt.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Mit der Methode #eval
kann Code auf der Bindung ausgewertet werden. Das obige Beispiel ist nicht sehr nützlich, da ein einfacher Aufruf von foo
das gleiche Ergebnis liefern würde. Da eine Bindung jedoch ein Objekt ist, das weitergegeben werden kann, kann sie für einige interessantere Dinge verwendet werden. Schauen wir uns ein Beispiel an.
Ein Beispiel aus dem wirklichen Leben
Nachdem wir nun in der Sicherheit unserer Garage etwas über Bindungen gelernt haben, nehmen wir sie mit auf die Piste und spielen im Schnee herum. Abgesehen von der spracheninternen Verwendung von Bindungen in Ruby, gibt es einige Situationen, in denen Bindungsobjekte explizit verwendet werden. Ein gutes Beispiel dafür ist ERB-Rubys Template-System.
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 diesem Beispiel erstellen wir eine Variable namens x
, eine Methode namens y
und ein ERB-Template, das beide referenziert. Dann übergeben wir die aktuelle Bindung an ERB#result
, die die ERB-Tags in der Vorlage auswertet und einen String mit den ausgefüllten Variablen zurückgibt.
Unter der Haube verwendet ERB Binding#eval
, um den Inhalt jedes ERB-Tags im Bereich der übergebenen Bindung auszuwerten. Eine vereinfachte Implementierung, die für das obige Beispiel funktioniert, könnte wie folgt aussehen:
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`" |
Die Klasse DiyErb
nimmt bei der Initialisierung einen Template-String an. Ihre #result
-Methode findet alle ERB-Tags und ersetzt sie durch das Ergebnis der Auswertung ihres Inhalts. Dazu ruft sie Binding#eval
auf der übergebenen Bindung mit dem Inhalt der ERB-Tags auf.
Durch die Übergabe der aktuellen Bindung beim Aufruf der #result
-Methode können die eval
-Aufrufe auf die außerhalb der Methode und sogar außerhalb der Klasse definierten Variablen zugreifen, ohne sie explizit übergeben zu müssen.
Haben wir Sie im Wald verloren?
Wir hoffen, Ihnen hat unser Skiausflug in den Wald gefallen. Wir haben uns intensiver mit dem Thema Geltungsbereich und Sperrung beschäftigt, nachdem wir es vorher nur gestreift haben. Wir hoffen, wir haben Sie nicht in den Wäldern verloren. Bitte lassen Sie uns wissen, wenn Sie mehr über Bindungen erfahren möchten oder ein anderes Ruby-Thema haben, in das Sie eintauchen möchten.
Danke, dass Sie uns gefolgt sind und bitte kicken Sie den Schnee von Ihren Bindungen, bevor Sie sie dem nächsten Entwickler überlassen. Wenn Ihnen diese magischen Ausflüge gefallen, möchten Sie vielleicht Ruby Magic abonnieren, um eine E-Mail zu erhalten, wenn wir etwa einmal im Monat einen neuen Artikel veröffentlichen.