Cała oryginalna treść jest tworzona po ukraińsku. Nie wszystkie treści zostały jeszcze przetłumaczone. Niektóre posty mogą być dostępne tylko po ukraińsku.Dowiedz się więcej

Zakres widoczności zmiennej lokalnej w Ruby

Okładka posta: Zakres widoczności zmiennej lokalnej w Ruby
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.

29 maj 09:09

Jakie systemy operacyjne wspierają Ruby?

meme code
meme code@memecode
Czy Ruby tworzy nową kopię obiektu podczas przypisywania zmiennej do zmiennej?
29 maj 09:30

Czy Ruby tworzy nową kopię obiektu podczas przypisywania zmiennej do zmiennej?

meme code
meme code@memecode
Jaka jest różnica między wartością bezpośrednią a referencją w Ruby?
29 maj 12:00

Jaka jest różnica między wartością bezpośrednią a referencją w Ruby?

meme code
meme code@memecode
Dlaczego kod Ruby zwraca nil po wykonaniu puts?
29 maj 20:30

Dlaczego kod Ruby zwraca nil po wykonaniu puts?

meme code
meme code@memecode
Jaka jest różnica między nil a false w Ruby?
29 maj 20:59

Jaka jest różnica między nil a false w Ruby?

meme code
meme code@memecode
Dlaczego pusty ciąg (string) w Ruby nie jest fałszem?
31 maj 14:39

Dlaczego pusty ciąg (string) w Ruby nie jest fałszem?

meme code
meme code@memecode
Czym różni się int od bigint w Ruby? Minimalne i maksymalne wartości.
13 cze 06:37

Czym różni się int od bigint w Ruby? Minimalne i maksymalne wartości.

meme code
meme code@memecode
Co oznacza błąd 'is out of range' w Ruby on Rails? Błąd zakresu - Liczba całkowita z limitem 4 bajtów
13 cze 07:18

Co oznacza błąd 'is out of range' w Ruby on Rails? Błąd zakresu - Liczba całkowita z limitem 4 bajtów

meme code
meme code@memecode
Czym jest niemutowalność i mutowalność?
19 cze 07:48

Czym jest niemutowalność i mutowalność?

meme code
meme code@memecode
[Ruby] Co zwróci wynik dodawania 10.5 i 10?
23 cze 13:23

[Ruby] Co zwróci wynik dodawania 10.5 i 10?

meme code
meme code@memecode
[Ruby] Czym różnią się zmienne zaczynające się od @, @@ i $?
23 cze 14:00

[Ruby] Czym różnią się zmienne zaczynające się od @, @@ i $?

meme code
meme code@memecode
Co to jest funkcja w programowaniu?
24 cze 18:15

Co to jest funkcja w programowaniu?

meme code
meme code@memecode