В 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 condition
# код
end
case variable
when value1
# код
when value2
# інший код
else
# інший код
end
while condition
# код
end
until condition
# код
end
for element in collection
# код
end
loop do
# код
break if condition
end
begin /
rescue /
ensure /
else
begin
# код
rescue SomeException => e
# обробка винятку
ensure
# код, який завжди виконується
else
# код, який виконується, якщо немає винятку
end
for i in 0..5
retry if i > 2
puts "i: #{i}"
end
begin
# код
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
Керуючі структури (if, while, for, тощо) не створюють нову область видимості, тому локальні змінні всередині них будуть доступні в оточуючому середовищі.
Приклади методів:
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
Методи (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