W Ruby nowy zasięg widoczności dla zmiennej lokalnej jest tworzony w kilku miejscach. Należy zrozumieć i poznać te miejsca.
- globalny kontekst (main);
- definicja klasy (lub modułu);
- definicja metody;
Rozważmy kilka przykładów ze strony ruby-lang.org oraz kilka nowych.
Stwórzmy plik example.rb z kodem
Globalny zasięg widoczności (main)
# Zmienna globalna jest definiowana poza jakimikolwiek klasami, metodami lub blokami.
var = 1 # Zmienna globalna
class Demo
var = 2 # Zmienna klasy
def method
var = 3 # Lokalne zmienna metody
puts "in method: var = #{var}"
end
puts "in class: var = #{var}"
end
puts "at top level: var = #{var}"
Demo.new.method
Uruchomienie skryptu 'ruby example.rb' pokaże nam następujący wynik:
# in class: var = 2
# at top level: var = 1
# in method: var = 3
Zwróć uwagę, że kod wewnątrz definicji klasy jest wykonywany w momencie jej tworzenia, dlatego komunikat wewnątrz klasy pojawia się od razu.
Bloki w Ruby i ich zasięg widoczności
W Ruby bloki ({ ... } lub do ... end) prawie tworzą nowy zasięg widoczności. Oznacza to, że lokalne zmienne zdefiniowane wewnątrz bloku są zazwyczaj niedostępne na zewnątrz. Jednak są pewne szczegóły. Zwróć uwagę na słowo 'prawie', które jest używane na stronie języka Ruby i czasami jest błędnie interpretowane przez nowicjuszy.
Przykład z blokiem
a = 0
1.upto(3) do |i|
a += i
b = i * i
end
puts a # => 6
puts b # Wystąpi błąd, ponieważ b nie jest zdefiniowana poza blokiem
W tym przykładzie zmienna a, która została zdefiniowana przed blokiem (konstrukcja do ... end), jest modyfikowana wewnątrz bloku, a te zmiany są widoczne na zewnątrz bloku. Z drugiej strony, zmienna b, która została zdefiniowana wewnątrz bloku, jest niedostępna na zewnątrz.
Bloki
prawie tworzą nowy zasięg widoczności, ponieważ lokalne zmienne zdefiniowane wewnątrz bloku nie mogą być dostępne z zewnątrz. Jednak jeśli zmienna już istnieje w zewnętrznym zasięgu przed wejściem do bloku, będzie dostępna zarówno wewnątrz bloku, jak i zmiany w niej zostaną zachowane po wyjściu z bloku.
Dlaczego "prawie"? Słowo "prawie" jest używane z kilku powodów (które już zostały opisane wcześniej):
- Zmienne zdefiniowane przed blokiem mogą być dostępne i modyfikowane wewnątrz bloku. Zmiany w nich są zachowywane po wyjściu z bloku.
- Zmienne zdefiniowane po raz pierwszy wewnątrz bloku są niedostępne na zewnątrz.
Te niuanse są szczególnie ważne do zapamiętania podczas pracy z wątkami i kodem asynchronicznym, gdzie zasięg widoczności może wpływać na dostępność zmiennych między różnymi częściami kodu.
Rozważmy jeszcze jeden przykład dla lepszego zrozumienia:
x = 10
[1, 2, 3].each do |i|
x += i
y = i * 2
end
puts x # => 16 (zmienna x zmienia się wewnątrz bloku)
puts y # Wystąpi błąd, ponieważ y nie jest zdefiniowana poza blokiem
Błąd dla 'puts y' będzie następujący:
(irb):8:in `<main>': undefined local variable or method `y' for main:Object (NameError)
puts y
^
from /Users/user/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/irb-1.13.1/exe/irb:9:in `<top (required)>'
from /Users/user/.rbenv/versions/3.2.1/bin/irb:25:in `load'
from /Users/user/.rbenv/versions/3.2.1/bin/irb:25:in `<main>'
W tym przykładzie zmienna x jest zdefiniowana przed blokiem i modyfikowana wewnątrz bloku. Zmienna y jest zdefiniowana wewnątrz bloku i niedostępna na zewnątrz.
To ilustruje, jak bloki "prawie" tworzą nowy zasięg widoczności, ale nie całkowicie oddzielają dostęp do zmiennych, które już istnieją w zewnętrznym zasięgu.
Przykład z wątkami (threads)
threads = []
["one", "two"].each do |name|
threads << Thread.new do
local_name = name
a = 0
3.times do |i|
Thread.pass
a += i
puts "#{local_name}: #{a}"
end
end
end
threads.each { |t| t.join }
Wynik w moim przypadku (wynik zależy od działania Thread.pass, systemu operacyjnego i procesora):
one: 0
two: 0
one: 1
one: 3
two: 1
two: 3
Tworzymy pustą tablicę threads, w której będziemy przechowywać wszystkie utworzone wątki. Pętla each przechodzi przez tablicę ciągów ["one", "two"], gdzie każdy element po kolei będzie dostępny w zmiennej name. Dla każdego elementu tablicy tworzony jest nowy wątek za pomocą Thread.new. Kod wewnątrz bloku do ... end będzie wykonywany w kontekście nowego wątku.
Zmienna local_name przyjmuje wartość z bieżącego elementu tablicy name. Zrobiono to, aby każdy wątek miał swoją własną lokalną kopię zmiennej name. Następnie tworzona jest lokalna zmienna a z początkową wartością 0. Następnie działa pętla. Pętla wykonuje się trzy razy. Na każdej iteracji mamy następujące działania:
- Wykonywane jest Thread.pass, które pozwala innym wątkom się wykonywać.
- Zmienna a zwiększa się o wartość i.
- Wyświetlana jest wartość local_name i bieżąca wartość a.
threads.each { |t| t.join }
Operator join zmusza główny wątek do oczekiwania na zakończenie każdego z utworzonych wątków. Jest to konieczne, aby wszystkie wątki zakończyły swoją pracę przed zakończeniem programu (skryptu).
Zarządzanie strukturami, metodami i ich zasięgami widoczności
Poniżej zostaną podane przykłady struktur sterujących, metod dla wizualnego wyjaśnienia działania zasięgów widoczności. Ruby ma różnorodne struktury sterujące i metody, które pozwalają zarządzać przepływem wykonania programu.
if /
elsif /
else
if condition
# kod
elsif another_condition
# inny kod
else
# inny kod
end
unless condition
# kod
end
case variable
when value1
# kod
when value2
# inny kod
else
# inny kod
end
while condition
# kod
end
until condition
# kod
end
for element in collection
# kod
end
loop do
# kod
break if condition
end
begin /
rescue /
ensure /
else
begin
# kod
rescue SomeException => e
# obsługa wyjątku
ensure
# kod, który zawsze jest wykonywany
else
# kod, który jest wykonywany, jeśli nie ma wyjątku
end
for i in 0..5
retry if i > 2
puts "i: #{i}"
end
begin
# kod
rescue
retry
end
for i in 0..5
next if i < 3
puts "i: #{i}"
end
for i in 0..5
break if i > 2
puts "i: #{i}"
end
Struktury sterujące (if, while, for, itd.) nie tworzą nowego zasięgu widoczności, dlatego lokalne zmienne wewnątrz nich będą dostępne w otaczającym środowisku.
Przykłady metod:
times
5.times do |i|
puts i
end
1.upto(5) do |i|
puts i
end
5.downto(1) do |i|
puts i
end
0.step(10, 2) do |i|
puts i
end
[1, 2, 3].each do |element|
puts element
end
result = [1, 2, 3].map do |element|
element * 2
end
puts result
result = [1, 2, 3, 4, 5].select do |element|
element.even?
end
puts result
result = [1, 2, 3, 4, 5].reject do |element|
element.even?
end
puts result
result = [1, 2, 3, 4, 5].find do |element|
element.even?
end
puts result
sum = [1, 2, 3, 4, 5].inject(0) do |accumulator, element|
accumulator + element
end
puts sum
Metody (times, each, itd.) często przyjmują bloki, które mogą tworzyć nowy zasięg widoczności dla zmiennych zdefiniowanych wewnątrz bloku.
Celowo dodałem wiele przykładów struktur sterujących i metod, aby pokazać, że można łatwo popełnić błąd i napotkać problem związany z zasięgiem widoczności. Głównym celem jest nie zapamiętywanie wszystkich metod, ale zapamiętanie różnicy między strukturami sterującymi a metodami. Ta wiedza pomoże
debugować potencjalnie problematyczne miejsca w kodzie.
Aby wizualnie pokazać, jak to wszystko działa, napiszemy testy:
require 'rspec'
RSpec.describe 'Zasięgi widoczności w Ruby' do
context 'Struktury sterujące' do
it 'tworzy nowy zasięg widoczności z if/elsif/else' do
if true
var = 1
end
expect(var).to eq(1)
end
it 'tworzy nowy zasięg widoczności z unless' do
unless false
var = 2
end
expect(var).to eq(2)
end
it 'tworzy nowy zasięg widoczności z case/when' do
case 1
when 1
var = 3
end
expect(var).to eq(3)
end
it 'tworzy nowy zasięg widoczności z while' do
i = 0
while i < 1
var = 4
i += 1
end
expect(var).to eq(4)
end
it 'tworzy nowy zasięg widoczności z until' do
i = 0
until i > 0
var = 5
i += 1
end
expect(var).to eq(5)
end
it 'tworzy nowy zasięg widoczności z for' do
for i in 0..0
var = 6
end
expect(var).to eq(6)
end
it 'tworzy nowy zasięg widoczności z loop' do
var = nil
loop do
var = 7
break
end
expect(var).to eq(7)
end
it 'obsługuje wyjątek z begin/rescue/ensure' do
var = 0
begin
raise 'błąd'
rescue
var = 1
ensure
var += 2
end
expect(var).to eq(3)
end
it 'obsługuje wyjątek z begin/rescue/else/ensure' do
var = 0
begin
var += 1
rescue
var += 2
else
var += 3
ensure
var += 4
end
expect(var).to eq(8)
end
it 'powtarza wykonanie z redo' do
var = 0
i = 0
for i in 0..5
if i < 2
var = i
break if i == 1 # Unikamy nieskończonej pętli
end
end
expect(var).to eq(1)
end
it 'powtarza wykonanie z retry' do
var = 0
attempts = 0
begin
raise 'błąd' if attempts < 1
rescue
attempts += 1
retry if attempts < 2
else
var = 9
end
expect(var).to eq(9)
end
it 'pomija iterację z next' do
var = []
for i in 0..5
next if i < 3
var << i
end
expect(var).to eq([3, 4, 5])
end
it 'wychodzi z pętli z break' do
for i in 0..5
break if i > 2
var = i
end
expect(var).to eq(2)
end
end
context 'Metody' do
it 'tworzy nowy zasięg widoczności z times' do
1.times do
var = 10
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z upto' do
1.upto(1) do
var = 11
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z downto' do
1.downto(1) do
var = 12
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z step' do
0.step(0, 1) do
var = 13
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z each' do
[1].each do
var = 14
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z map' do
[1].map do
var = 15
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z select' do
[1].select do
var = 16
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z reject' do
[1].reject do
var = 17
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z find' do
[1].find do
var = 18
end
expect(defined?(var)).to be_nil
end
it 'tworzy nowy zasięg widoczności z inject/reduce' do
[1].inject(0) do |acc, _|
var = 19
end
expect(defined?(var)).to be_nil
end
end
end
Wszystkie one zakończyły się sukcesem:
Finished in 0.06456 seconds (files took 0.26825 seconds to load)
23 examples, 0 failures