Cieszymy się nowym rokiem i witamy z powrotem w Magii Rubiego! W tym zimowym odcinku zajmiemy się wiązaniami i zakresami leksykalnymi. Więc załóżcie narty i podążajcie za nami w głąb lasu.
Poprzednim razem spojrzeliśmy na domknięcia w Rubim porównując bloki, procs i lambdas. Oprócz różnic pomiędzy tymi trzema typami, poruszyliśmy temat tego, co definiuje domknięcie.
Domknięcie jest funkcją pierwszej klasy z otoczeniem. Środowisko jest mapowaniem do zmiennych, które istniały, gdy zamknięcie zostało utworzone. Domknięcie zachowuje dostęp do tych zmiennych, nawet jeśli są one zdefiniowane w innym zakresie.
Poznaliśmy już odpowiednik funkcji pierwszej klasy w Rubim, ale wygodnie pominęliśmy środowiska. W tym odcinku przyjrzymy się jak to środowisko działa dla domknięć, klas i instancji klas poprzez zbadanie jak Ruby radzi sobie z zakresem leksykalnym poprzez swoje wiązania.
Zakres leksykalny
W programowaniu, zakres odnosi się do wiązań dostępnych w określonej części kodu. Wiązanie, lub wiązanie nazwy, wiąże nazwę z odniesieniem do pamięci, jak nazwa zmiennej do jej wartości. Zakres definiuje, co self
oznacza, metody, które mogą być wywoływane, i zmienne, które są dostępne.
Ruby, podobnie jak większość nowoczesnych języków programowania, używa statycznego zakresu, często nazywanego zakresem leksykalnym (w przeciwieństwie do zakresu dynamicznego). Aktualny zakres jest oparty na strukturze kodu i określa zmienne dostępne w określonych częściach kodu. Oznacza to, że zakres zmienia się, gdy kod przeskakuje między metodami, blokami i klasami – ponieważ wszystkie one mogą mieć różne zmienne lokalne, na przykład.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
W tej metodzie tworzymy zmienną lokalną wewnątrz metody i wypisujemy ją na konsolę. Zmienna jest w zakresie wewnątrz metody, ponieważ jest tam tworzona.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
W tym przykładzie tworzymy zmienną poza metodą. Kiedy wywołamy zmienną wewnątrz metody, otrzymamy błąd, ponieważ zmienna jest poza zakresem. Zmienne lokalne mają ścisły zakres, co oznacza, że metoda nie może uzyskać dostępu do zmiennej poza nią samą, chyba że zostanie ona przekazana jako argument.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Podczas gdy zmienne lokalne są dostępne lokalnie, zmienne instancji są dostępne dla wszystkich metod instancji klasy.
Inherited Scopes and Proc Bindings
Jak widzieliśmy w poprzednich przykładach, zakres jest oparty na lokalizacji w kodzie. Zmienna lokalna zdefiniowana poza metodą nie ma zakresu wewnątrz metody, ale może być udostępniona przez przekształcenie jej w zmienną instancji. Metody nie mogą uzyskać dostępu do zmiennych lokalnych zdefiniowanych poza nimi, ponieważ metody mają swój własny zakres, z własnymi wiązaniami.
Procesy (w tym bloki i lambdy, przez rozszerzenie) są inne. Ilekroć proc jest instancjonowany, tworzone jest wiązanie, które dziedziczy referencje do zmiennych lokalnych w kontekście, w którym blok został utworzony.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
W tym przykładzie ustawiamy zmienną o nazwie foo
na 1
. Wewnętrznie, obiekt Proc utworzony w drugiej linii tworzy nowe wiązanie. Wywołując proc, możemy zapytać o wartość zmiennej.
Ponieważ wiązanie jest tworzone podczas inicjalizacji proc, nie możemy utworzyć proc przed zdefiniowaniem zmiennej, nawet jeśli blok jest wywoływany dopiero po zdefiniowaniu zmiennej.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Wywołanie proc da NameError
, gdyż zmienna nie jest zdefiniowana w wiązaniach proc. Tak więc, wszelkie zmienne, do których mamy dostęp w proc powinny być zdefiniowane przed utworzeniem proc lub przekazaniem go jako argumentu.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Możemy jednak zmienić zmienną po jej zdefiniowaniu w głównym kontekście, ponieważ wiązanie proc posiada do niej referencję zamiast ją kopiować.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
W tym przykładzie widzimy, że zmienna foo
wskazuje na ten sam obiekt, gdy jest w proc, jak i poza nim. Możemy ją zaktualizować wewnątrz procesu, aby zmienna poza nim również została zaktualizowana.
Wiązania
Aby śledzić bieżący zakres, Ruby używa wiązań, które hermetyzują kontekst wykonania w każdej pozycji w kodzie. Metoda binding
zwraca obiekt Binding
, który opisuje wiązania w bieżącej pozycji.
1 2 |
foo = 1 binding.local_variables # => |
Obiekt wiązania posiada metodę o nazwie #local_variables
, która zwraca nazwy wszystkich zmiennych lokalnych dostępnych w bieżącym zakresie.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Kod może być obliczany na wiązaniu za pomocą metody #eval
. Powyższy przykład nie jest zbyt użyteczny, ponieważ zwykłe wywołanie foo
dałoby ten sam rezultat. Jednakże, ponieważ wiązanie jest obiektem, który może być przekazywany wokół, może być używany do bardziej interesujących rzeczy. Przyjrzyjmy się przykładowi.
Przykład z prawdziwego życia
Teraz, gdy poznaliśmy już wiązania w bezpiecznym garażu, możemy zabrać je na stok i pobawić się na śniegu. Poza wewnętrznym użyciem wiązań w Rubim, istnieją pewne sytuacje, w których wiązania są używane jawnie. Dobrym przykładem jest system szablonów 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`" |
W tym przykładzie tworzymy zmienną o nazwie x
, metodę o nazwie y
i szablon ERB, który odwołuje się do obu. Następnie przekazujemy bieżące wiązanie do ERB#result
, który ocenia znaczniki ERB w szablonie i zwraca łańcuch znaków z wypełnionymi zmiennymi.
Pod maską, ERB używa Binding#eval
do oceny zawartości każdego znacznika ERB w zakresie przekazanego wiązania. Uproszczona implementacja, która działa dla powyższego przykładu, mogłaby wyglądać następująco:
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`" |
Klasa DiyErb
pobiera przy inicjalizacji łańcuch szablonu. Jej metoda #result
znajduje wszystkie znaczniki ERB i zastępuje je wynikiem oceny ich zawartości. Aby to zrobić, wywołuje Binding#eval
na przekazanym wiązaniu, z zawartością znaczników ERB.
Przekazując bieżące wiązanie podczas wywoływania metody #result
, wywołania eval
mogą uzyskać dostęp do zmiennych zdefiniowanych poza metodą, a nawet poza klasą, bez konieczności ich jawnego przekazywania.
Czy zgubiliśmy Cię w lesie?
Mamy nadzieję, że podobała Ci się nasza narciarska wycieczka do lasu. Weszliśmy głębiej w zakresy i zamknięcia, po tym jak je przeoczyliśmy. Mamy nadzieję, że nie zgubiliśmy Cię w lesie. Daj nam znać jeśli chciałbyś dowiedzieć się więcej o wiązaniach, lub masz jakiś inny temat związany z Rubinem, który chciałbyś zgłębić.
Dziękujemy za śledzenie nas i prosimy o odkopanie śniegu z Twoich wiązań zanim zostawisz je dla następnego dewelopera. Jeśli podobają Ci się te magiczne podróże, możesz zasubskrybować Magię Rubiego aby otrzymywać e-mail gdy opublikujemy nowy artykuł mniej więcej raz w miesiącu.