Глава сьома
Цикли та ітератори…
Багато чого у програмуванні пов’язане з діями, які повторюються. Можливо, вам знадобиться програма, яка подаватиме звуковий сигнал 10 разів, читатиме рядки з файлу, поки у ньому є що читати, або показуватиме повідомлення, поки користувач не натисне клавішу. Ruby надає вам кілька способів виконати такі повторення.
Цикл for
У багатьох мовах програмування, коли ви хочете запустити код певну кількість разів, ви можете просто вставити цей код всередину циклу for
. У більшості мов ви маєте дати циклу for
змінну ініціалізовану з початковим значенням, яка буде збільшуватись на 1 на кожній ітерації, поки вона не досягне певного кінцевого значення. Коли кінцеве значення досягнуто, цикл for
припиняє виконання. Ось традиційна версія циклу for
написана мовою Pascal:
(* Це код на Pascal, а не на Ruby! *)
for i := 1 to 3 do
writeln(i);
З Глави 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
:
[1, 2, 3].each do |i|
puts(i)
end
Як бачите, між ними справді немає суттєвої відмінності.
Щоб перетворити цикл for
на ітератор each
, все що мені потрібно зробити — це видалити for
та додати після масиву .each
. Потім я передаю змінну ітератора i
, яка стоїть між двома символами вертикальних рисок.
Порівняйте ці приклади, щоб побачити, наскільки цикл for
та ітератор each
схожі між собою:
# --- Приклад 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
…Якщо вам знадобиться більш традиційний цикл
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
так…
# 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 # поки втомлений
Ось приклад, який показує різні альтернативні записи:
$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
, код ніколи не виконається, якщо булева умова відразу є істинною.
Коли цикл виконується принаймні один раз
Зазвичай, цикл
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
:
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