Зміст дописунатисність на посилання, щоб перейти до потрібного місця
В Ruby нова область видимості для локальної змінної створюється в декількох місцях. Треба зрозуміти і вивчити ці місця.
- глобальний контекст (main);
- визначення класу (або модуля);
- визначення методу;
Розглянемо кілька прикладів з сайту ruby-lang.org та декілька нових.Створимо файл example.rb з кодом
Глобальна область видимості (main)
# Глобальна змінна визначається за межами будь-яких класів, методів або блоків. var = 1 # Глобальна змінна class Demo var = 2 # Змінна класу def method var = 3 # Локальна змінна методу puts "in method: var = #{var}" end puts "in class: var = #{var}" end puts "at top level: var = #{var}" Demo.new.method
Запуск скрипту 'ruby example.rb' покаже нам наступний результат:
# in class: var = 2 # at top level: var = 1 # in method: var = 3
Зверніть увагу, що код всередині визначення класу виконується під час його створення, тому повідомлення всередині класу з'являється одразу.
Блоки в Ruby та їх область видимості
У Ruby блоки ({ ... } або do ... end) майже створюють нову область видимості. Це означає, що локальні змінні, визначені всередині блоку, зазвичай недоступні за його межами. Однак є деякі особливості. Зверніть увагу на слово 'майже', воно використовується на сторінці сайту мови Ruby, та іноді неправильно інтерпретується новачками.
Приклад з блоком
a = 0 1.upto(3) do |i| a += i b = i * i end puts a # => 6 puts b # Виникне помилка, бо b не визначена поза блоком
У цьому прикладі змінна a, яка була визначена до блоку (конструкція do ... end), модифікується всередині блоку, і ці зміни видно за межами блоку. З іншого боку, змінна b, яка була визначена всередині блоку, недоступна за його межами.
Блоки майже створюють нову область видимості, оскільки локальні змінні, визначені всередині блоку, не можуть бути доступні ззовні. Однак, якщо змінна вже існує в зовнішній області видимості до входу в блок, вона буде доступна і всередині блоку, і зміни до неї збережуться після виходу з блоку.
Чому "майже"? Слово "майже" використовується з декількох причин (які були вже описані до цього):
- Змінні, визначені до блоку, можуть бути доступні і модифіковані всередині блоку. Зміни до них зберігаються після виходу з блоку.
- Змінні, визначені вперше всередині блоку, недоступні за його межами.
Ці нюанси особливо важливо пам'ятати при роботі з потоками та асинхронним кодом, де область видимості може впливати на доступність змінних між різними частинами коду.
Розглянемо ще один приклад для кращого розуміння:
x = 10 [1, 2, 3].each do |i| x += i y = i * 2 end puts x # => 16 (змінна x змінюється всередині блоку) puts y # Виникне помилка, бо y не визначена поза блоком
Помилка для 'puts y' буде наступна:
(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>'
У цьому прикладі змінна x визначена до блоку і модифікується всередині блоку. Змінна y визначена всередині блоку і недоступна поза ним. Це ілюструє, як блоки "майже" створюють нову область видимості, але не зовсім відокремлюють доступ до змінних, які вже існують у зовнішній області видимості.
Приклад з потоками (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 }
Результат в моєму випадку (результат залежить від роботи Thread.pass, операційної системи та процесору):
one: 0 two: 0 one: 1 one: 3 two: 1 two: 3
Ми створюємо порожній масив threads, куди будемо зберігати всі створені потоки. Цикл each проходить по масиву рядків ["one", "two"], де кожен елемент по черзі буде доступний у змінній name. Для кожного елементу масиву створюється новий потік за допомогою Thread.new. Код всередині блоку do ... end буде виконуватись у контексті нового потоку.
Змінна local_name отримує значення з поточного елементу масиву name. Це зроблено для того, щоб кожен потік мав свою власну локальну копію змінної name. Далі створюється локальна змінна a з початковим значенням 0. Наступним працює цикл. Цикл виконується три рази. На кожній ітерації маємо наступні дії:
- Виконується Thread.pass, який дозволяє іншим потокам виконуватись.
- Змінна a збільшується на значення i.
- Виводиться значення local_name і поточне значення a.
І фінішуємо:
threads.each { |t| t.join }
Оператор join змушує головний потік очікувати завершення кожного з створених потоків. Це необхідно, щоб всі потоки завершили свою роботу до завершення програми (скрипту).
Керуючи структури, методи та їх області видимості
Нижче будуть наведені приклади керуючих структур, методів для наочного пояснення роботи областей видимості. Ruby має різноманітні керуючі структури та методи, які дозволяють керувати потоком виконання програми.
if / elsif / else
if condition # код elsif another_condition # інший код else # інший код end
unless
unless condition # код end
case / when
case variable when value1 # код when value2 # інший код else # інший код end
while
while condition # код end
until
until condition # код end
for
for element in collection # код end
loop
loop do # код break if condition end
begin / rescue / ensure / else
begin # код rescue SomeException => e # обробка винятку ensure # код, який завжди виконується else # код, який виконується, якщо немає винятку end
redo
for i in 0..5 retry if i > 2 puts "i: #{i}" end
retry
begin # код 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
Керуючі структури (if, while, for, тощо) не створюють нову область видимості, тому локальні змінні всередині них будуть доступні в оточуючому середовищі.
Приклади методів:
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
Методи (times, each, тощо) часто приймають блоки, які можуть створювати нову область видимості для змінних, визначених всередині блоку.
Я навмисно додав багато прикладів керуючих структур та методів, щоб показати, що тут можна легко помилитись та отримати проблему пов'язану з областю видимості. Головна мета - не запам'ятати всі методи, а запам'ятати різницю між керуючими структурами та методами. Саме це знання допоможе дебажити потенційно проблемні місця в коді.
Щоб наочно показати як це все працює, напишемо тести:
require 'rspec' RSpec.describe 'Області видимості в Ruby' do context 'Керуючі структури' do it 'створює нову область видимості з if/elsif/else' do if true var = 1 end expect(var).to eq(1) end it 'створює нову область видимості з unless' do unless false var = 2 end expect(var).to eq(2) end it 'створює нову область видимості з case/when' do case 1 when 1 var = 3 end expect(var).to eq(3) end it 'створює нову область видимості з while' do i = 0 while i < 1 var = 4 i += 1 end expect(var).to eq(4) end it 'створює нову область видимості з until' do i = 0 until i > 0 var = 5 i += 1 end expect(var).to eq(5) end it 'створює нову область видимості з for' do for i in 0..0 var = 6 end expect(var).to eq(6) end it 'створює нову область видимості з loop' do var = nil loop do var = 7 break end expect(var).to eq(7) end it 'обробляє виняток з begin/rescue/ensure' do var = 0 begin raise 'помилка' rescue var = 1 ensure var += 2 end expect(var).to eq(3) end it 'обробляє виняток з 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 'повторює виконання з redo' do var = 0 i = 0 for i in 0..5 if i < 2 var = i break if i == 1 # Уникаємо нескінченного циклу end end expect(var).to eq(1) end it 'повторює виконання з retry' do var = 0 attempts = 0 begin raise 'помилка' if attempts < 1 rescue attempts += 1 retry if attempts < 2 else var = 9 end expect(var).to eq(9) end it 'пропускає ітерацію з next' do var = [] for i in 0..5 next if i < 3 var << i end expect(var).to eq([3, 4, 5]) end it 'виходить з циклу з break' do for i in 0..5 break if i > 2 var = i end expect(var).to eq(2) end end context 'Методи' do it 'створює нову область видимості з times' do 1.times do var = 10 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з upto' do 1.upto(1) do var = 11 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з downto' do 1.downto(1) do var = 12 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з step' do 0.step(0, 1) do var = 13 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з each' do [1].each do var = 14 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з map' do [1].map do var = 15 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з select' do [1].select do var = 16 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з reject' do [1].reject do var = 17 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з find' do [1].find do var = 18 end expect(defined?(var)).to be_nil end it 'створює нову область видимості з inject/reduce' do [1].inject(0) do |acc, _| var = 19 end expect(defined?(var)).to be_nil end end end
Всі вони успішно виконались:
Finished in 0.06456 seconds (files took 0.26825 seconds to load) 23 examples, 0 failures