Spis treściKliknij link, aby przejść do wybranego miejsca
Ta treść została automatycznie przetłumaczona z ukraińskiego.
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.
I kończymy:
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
unless condition # kod end
case / when
case variable when value1 # kod when value2 # inny kod else # inny kod end
while
while condition # kod end
until
until condition # kod end
for
for element in collection # kod end
loop
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
redo
for i in 0..5
retry if i > 2
puts "i: #{i}"
end
retry
begin # kod rescue retry end
next
for i in 0..5
next if i < 3
puts "i: #{i}"
end
break
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
upto
1.upto(5) do |i| puts i end
downto
5.downto(1) do |i| puts i end
step
0.step(10, 2) do |i| puts i end
each
[1, 2, 3].each do |element| puts element end
map
result = [1, 2, 3].map do |element| element * 2 end puts result
select
result = [1, 2, 3, 4, 5].select do |element| element.even? end puts result
reject
result = [1, 2, 3, 4, 5].reject do |element| element.even? end puts result
find
result = [1, 2, 3, 4, 5].find do |element| element.even? end puts result
inject/reduce
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
Ten post nie ma jeszcze żadnych dodatków od autora.