Область видимості локальної змінної в Ruby
Дисклеймер

Інформація на сайті tseivo.com є суб'єктивною та відображає особисті погляди та досвід авторів та авторок блогів.

Використовуйте цей ресурс як одне з декількох джерел інформації під час своїх досліджень та прийняття рішень. Завжди застосовуйте критичне мислення. Людина сама несе відповідальність за свої рішення та дії.

Обкладинка нотатки: Область видимості локальної змінної в Ruby

Область видимості локальної змінної в Ruby

В 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

🙌 Підтримати блог @memecode

Ви можете поширити цей допис у соцмережах, чим допоможете платформі цейво розвиватись (* ^ ω ^)

📝 Більше публікацій: