Глава сьома

Цикли та ітератори…

Багато чого у програмуванні пов’язане з діями, які повторюються. Можливо, вам знадобиться програма, яка подаватиме звуковий сигнал 10 разів, читатиме рядки з файлу, поки у ньому є що читати, або показуватиме повідомлення, поки користувач не натисне клавішу. Ruby надає вам кілька способів виконати такі повторення.

Цикл for

У багатьох мовах програмування, коли ви хочете запустити код певну кількість разів, ви можете просто вставити цей код всередину циклу for. У більшості мов ви маєте дати циклу for змінну ініціалізовану з початковим значенням, яка буде збільшуватись на 1 на кожній ітерації, поки вона не досягне певного кінцевого значення. Коли кінцеве значення досягнуто, цикл for припиняє виконання. Ось традиційна версія циклу for написана мовою Pascal:

(* Це код на Pascal, а не на Ruby! *)
for i := 1 to 3 do
  writeln(i);

for_loop.rb:

З Глави 5 (про масиви) ви можете пам’ятати, що цикл for у Ruby працює зовсім іншим чином! Замість того, щоб передавати початкове та кінцеве значення, ми передаємо циклу for список елементів, і він ітерується по ньому від одного до іншого, присвоюючи значення на кожній ітерації певні змінній циклу. Це триває аж поки він не досягне кінця списку.

Наприклад, ось цикл for, що ітерується по елементах масиву, виводячи їх на екран:

# А ось це вже код на Ruby…
for i in [1, 2, 3] do
  puts(i)
end

Цикл for більше схожий на ітератор for each, який пропонують деякі інші мови програмування. Так і є, автор Ruby описує for як синтаксичний цукор для методу each, який імплементують типи колекцій у Ruby, такі як Array (масиви), Set (множини), Hash (хеші) та String (рядки) (самі рядки є колекціями символів).

Для порівняння, ось як виглядатиме цикл for з прикладу вище, якщо переписати його з допомогою методу each:

each_loop.rb:

[1, 2, 3].each do |i|
  puts(i)
end

Як бачите, між ними справді немає суттєвої відмінності.

Щоб перетворити цикл for на ітератор each, все що мені потрібно зробити — це видалити for та додати після масиву .each. Потім я передаю змінну ітератора i, яка стоїть між двома символами вертикальних рисок.

Порівняйте ці приклади, щоб побачити, наскільки цикл for та ітератор each схожі між собою:

for_each.rb:

# --- Приклад 1 ---

# i) for
for s in ['один', 'два', 'три'] do
  puts(s)
end

# ii) each
['один', 'два', 'три'].each do |s|
  puts(s)
end

# --- Приклад 2 ---

# i) for
for x in [1, "два", [3, 4, 5]] do puts(x) end

# ii) each
[1, "два", [3, 4, 5]].each do |x| puts(x) end

Зверніть увагу, що ключове слово do є необов’язковим для циклу for, який займає декілька рядків, проте воно необхідне, коли цикл записаний у одному рядку:

# Тут ключове слово "do" можна упустити
for s in ['один', 'два', 'три']
  puts(s)
end

# Проте тут воно є обов’язковим
for s in ['один', 'два', 'три'] do puts(s) end

for_to.rb:

Як написати звичайний цикл for

Якщо вам знадобиться більш традиційний цикл for, ви завжди можете зімітувати його в Ruby, якщо цикл for ітеруватиме по значеннях проміжку. Наприклад, ось як можна використати змінну циклу for, щоб порахувати від 1 до 10, виводячи її значення на кожній ітерації циклу:

for i in (1..10) do
  puts(i)
end

Це може бути перезаписано з використанням each:

(1..10).each do |i|
  puts(i)
end

Тим не менше, зверніть увагу на те, що вираз проміжку, як от 1..3, повинен бути записаний між двома круглими дужками, при використанні методу each, в іншому випадку Ruby подумає, що ви намагаєтесь використати each, як метод останнього з чисел (FixNum), замість цілого виразу (Range). Проте дужки є необов’язковими, якщо використовувати проміжок з циклом for.

При ітерації по значення з допомогою each, блок коду між do та end називається (очевидно, чи не так?) блоком ітератора.

Параметри блоку. У Ruby будь–які змінні, які оголошені між вертикальними рисками на початку блоку, називаються параметрами блоку. Крім цього, блок працює як функція, а параметри блоку — як список аргументів функції. Метод each запускає код всередині блоку і передає в нього аргументи, які отримуються з колекції (наприклад, з масиву).

Блоки

Ruby має альтернативний синтаксис для виокремлення блоків. Ви можете використовувати do..end так…

block_syntax.rb:

# do..end
[[1,2,3], [3,4,5], [6,7,8]].each do |a,b,c|
  puts("#{a}, #{b}, #{c}")
end

Або користуватись фігурними дужками {..} ось так:

# curly braces {..}
[[1,2,3], [3,4,5], [6,7,8]].each{ |a,b,c|
  puts("#{a}, #{b}, #{c}")
}

Не важливо, які позначення ви використовуєте, ви маєте бути певні лише в тому, що відкриваюче позначення, { або do, розміщене на тому ж рядку, що і метод each. Перенесення на новий рядок між each та відкриваючим позначенням спричинить синтаксичну помилку.

Цикл while

Ruby також має декілька інших конструкцій для циклів. Ось як можна працювати з циклом while (поки):

while tired     # поки втомлений
  sleep         #   спати
end             #

Або, записуючи по іншому:

sleep while tired   # спати поки втомлений

Ці два приклади виконують одні і ті ж дії, не дивлячись на те, що їх синтаксис відрізняється. У першому прикладі, код між while та end (тут це виклик методу sleep) виконується, поки тестова булева умова (яка, у цьому випадку, є значенням, яке повертається методом tired) залишається істинною.

Як і в циклах for, ключове слово do між тестовою умовою та кодом, який має виконуватись, якщо вони не стоять на одному рядку, є необов’язковим; ключове слово do, відповідно, обов’язкове, якщо тестова умова і код знаходяться на одному рядку.

Модифікатори while

У іншій версії циклу (sleep while tired), код, який має виконуватись (sleep), стоїть перед тестовою умовою (while tired). Такий синтаксис називається модифікатором while. Якщо ви хочете виконати кілька виразів, використовуючи такий запис, ви можете огорнути їх ключовими словами begin та end:

begin            #
  sleep          #   спати
  snore          #   хропіти
end while tired  # поки втомлений

Ось приклад, який показує різні альтернативні записи:

while.rb:

$hours_asleep = 0

def tired
  if $hours_asleep >= 8 then
    $hours_asleep = 0
    return false
  else
    $hours_asleep += 1
    return true
  end
end

def snore
  puts('храп....')
end

def sleep
  puts("z" * $hours_asleep)
end

while tired do sleep end  # однорядковий цикл while

while tired               # багаторядковий цикл while
  sleep
end

sleep while tired         # однорядковий модифікатор while

begin                     # багаторядковий модифікатор while
  sleep
  snore
end while tired

Останній приклад (багаторядковий модифікатор while modifier) потребує детальнішого розгляду, оскільки він відкриває для нас нову важливу поведінку. Якщо блок коду, що визначається через begin та end, передує перевірці while, цей код виконається принаймні один раз. У інших циклах while, код ніколи не виконається, якщо булева умова відразу є істинною.

while2.rb:

Коли цикл виконується принаймні один раз

Зазвичай, цикл while виконується 0 або більше разів, оскільки булева перевірка виконується до того, як виконається сам цикл; якщо перевірка повертає false, код всередині циклу не виконається ніколи.

Однак, якщо перевірка while слідує після коду, який огорнутий в begin та end, цикл виконується 1 або більше разів, тому що булевий вираз виконується after коду всередині циклу.

Щоб відчути різницю між поведінкою цих двох типів циклу while, запустіть while2.rb. Ці приклади допоможуть зрозуміти краще:

x = 100

# Код у цьому циклі ніколи не виконається
while (x < 100) do puts('x < 100') end

# Код у цьому циклі ніколи не виконається
puts('x < 100') while (x < 100)

# Код у цьому циклі виконається один раз
begin puts('x < 100') end while (x < 100)

Цикл until

Ruby також має цикл until, який можна трактувати як цикл поки не. Його синтаксис та параметри такі ж, як і у while – тобто тестова перевірка та код можуть розташовуватись на одному рядку (звісно, якщо між ними є ключове слово do) або на декількох рядках (тоді do необов’язкове). Також, є модифікатор until, який дає вам можливість розмістити код до тестової перевірки. Якщо ж ви огортаєте код між begin та end, пам’ятайте, що він виконається щонайменше один раз.

until.rb:

Ось прості приклади з циклом until:

i = 10

until i == 10 do puts(i) end  # ніколи не виконається

until i == 10                 # ніколи не виконається
  puts(i)
  i += 1
end

puts(i) until i == 10         # ніколи не виконається

begin                         # виконається один раз
  puts(i)
end until i == 10

Обидва цикли while та until, так само як і цикл for, можна використовувати для ітерування по масивах та інших колекціях. Наприклад, ось як можна ітеруватись по всіх елементах масиву:

while i < arr.length
  puts(arr[i])
  i += 1
end

until i == arr.length
  puts(arr[i])
  i +=1
end

results matching ""

    No results matching ""