Циклы, массивы и итераторы
При промышленной разработке нам зачастую необходимо обрабатывать большие наборы данных. Для решения соответствующих задач на Ruby необходимо разобраться как с базовыми структурами данных, массивами, так и со средствами выполненя кода несколько раз, то есть с циклами. Однако стоит отметить то, что в идеоматическом коде на Ruby почти не используются циклы - их эффективно замеяют итераторы и нумераторы.
Описание циклов и базовых итераторов доступно в официальной документации
Циклы
Циклы while
, until
Циклы позволяют выполнять некоторый блок кода в соответствии с условием. Цикл while
выполняется пока условие верно, цикл until
выполняется пока условие не является верым. Выведем числа от 0 до 10 с помощью цикла while
.
a = 0
while a < 10
puts a
a += 1
end
puts a
Выведем числа от 1 до 11 с помощью цикла until
.
a = 0
until a > 10
puts a
a += 1
end
puts a
Результатом выражения, описывающего цикл, является nil
за исключением случая, когда вы прерываете выполнение цикла с помощью ключевого слова break
.
Модификаторы until
и while
Данные циклы, как и условные операторы if
и unless
можно использовать в формате модификаторов выражений. Выражение будет выполняться до тех пор пока условие верно или неверено.
a = 0
a += 1 while a < 10
puts a # Выводит 10
a = 0
a += 1 until a > 10
puts a # Выводит 11
С их помощью можно организовать выполнение блока кода хотя бы 1 раз перед проверкой условия. Для этого используем описание блока с помощью конструкции begin
-end
. Организуем вывод чисел от 0 до 10 с помощью такой формы цикла.
a = 0
begin
puts a
a += 1
end while a < 10
puts a
Цикл for
Данный цикл предназначен для прохождения по элементам массива. В настоящее время не используется, так как инструмент значительно слабее итераторов, мы не будем его рассматривать. Не рекомендуется к использованию в языке Ruby.
for value in [1, 2, 3] do
puts value
end
Бесконечный итератор loop
Зачастую мы не можем «красиво» описать условие выхода из цикла или их может быть несколько. В такой ситуации можно использовать бесконечный итератор loop
, который будет выполнять ассоциированный с ним блок бесконечно. Организуем вывод чисел от 0 до 10 с помощью такого цикла.
a = 0
loop do
puts a
a += 1
break if a > 10
end
Управляющие слова
Прерывание циклов с помощью break
Ключевое слово break
позволяет прервать выполнение текущего цикла, а также любого итератора. Вы можете также передать данные, которые станут результатом выражения-цикла. Для итераторов это скорее всего не сработает.
Итератор прервёт свою работу в том случае, если элемент массива values
является чётным.
values.each do |value|
break if value.even?
# ...
end
Вариант возвращения результата из итератора. Находим квадрат первого чётного числа в массиве.
result = [1, 2, 3].each do |value|
break value * 2 if value.even?
end
puts result # prints 4
Однако стоит знать, что если в массиве не было ли одного чётного числа, тогда результатом работы данного кода станет ссылка на массив, по которому мы выполняли итерацию. Если вам необходимо вернуть результат, то лучше воспользоваться соответствующим итератором вместо использования break
. Возможные варианты подходящего итератора:
- Enumerable#find - поиск подходящего элемента
- Enumerable#find_all - поиск всех подходящих элементов
- Enumerable#find_index - поиск номера подходящего элемента
Методы примеси Enumerable доступны в классах Array и Hash.
Переход к следующей итерации с помощью next
Вызов next
прерывает дальнейшее выполнение кода в текущей итерации и переходит к выполнению следующей итерации в цикле или итераторе. next
также как и break
позволяет определить результат текущей итерации. Это свойство в отличие от предыдущего может быть использовано в повседневном программировании.
Например, в итераторе map
мы создаём новый массив, который содержит в себе удвоенные значения для всех нечётных чисел. Чётные числа не изменяются.
result = [1, 2, 3].map do |value|
next value if value.even?
value * 2
end
print result # Выводит массив [2, 2, 6]
Массивы
Массивы в Ruby всегда являются динамическими, т.е. можно менять не только их содержимое, но и количество элементов, содержащихся в массиве. В результате этот класс является базовым блоком при реализации почти любого приложения.
Помимо, собственно, хранения информации класс предоставляет возможности по удобной обработке этих элементов. Вы можете:
- создавать новые массивы на основании оригинального, например получить квадраты всех чисел в массиве;
- считать некотору общую характеристику для всех элементов массива, напирмер их сумму;
- фильтровать элементы в соответствии с некоторым критерием, например выделять положительные числа.
Это всё возможно с помощью методов, которые встроены в класс массив, что зачастую уменьшает необходимость в том, чтобы использовать циклы для обработки данных.
Объявление массива
Для создания массива обычно используются следующие подходы:
- Создание пустого массива с помощью литерала
[]
. - Преобразование другого объекта в массив с помощью метода
#to_a
. Для различных классов этот метод рабоатет по-разному. - Использование специальных методов для создания массивов строк
%w()
и%W()
.%w(foo bar baz #{1+1}) == ["foo", "bar", "baz", "\#{1+1}"] %W(foo bar baz #{1+1}) == ["foo", "bar", "baz", "2"]
- Ипользование специальных методов для создания массивов симоволов
%i()
.%i(address city) == [:address :city]
Методы для изменения содержимого массива
Базовыми действиями, которые мы хотим выполнять с массивами: добавлять и удалять данные, а также получать доступ к ним. Для решения последней задачи удобно использовать метод #[]
, который позволяет получить доступ как к одному элементу, так и выборке. Для изменения элемента по его номеру можно использовать метод #[]=
.
Для добавления данных в массив существуют следующие методы:
#push
или#append
добавляет в конец массива 1 или несколько элементов.#unshift
или#prepend
добавляет 1 или несколько элементов в начало массива.#<<
добавляет ровно 1 элемент в конец массива.
Для удаления элемента из массива существуют следующие методы:
#pop
удаляет 1 или несколько элементов с конца массива и возвращает его в результате своего выполнения. Если удалено несколько элементов, то он вернёт новый массив.#shift
удаляет 1 или несколько элементов с начала массива. Если было удалено больше одного элемента, то метод вернёт массив.#delete_at
позволяет удалить 1 элемент по определённому адресу. Метод возвращает удалённый элемент.
Все рассмотренные тут методы изменяют текущий массив. Однако при выполнении сложных обработок зачастую удобнее создать один или несколько промежуточных массивов, содержащих результаты обработки данных. Конечно, такой подход не будет «оптимальным» с точки зрения эффективности использования ресурсов, однако он зачастую может оказаться более читаемым для программиста. А ведь программы в первую очередь пишутся для людей, а уже в следующую очередь для интерпретатора.
Итераторы
Итератором в Ruby называется метод, который вызывает блок кода ассоциированный с данным методом в момент вызова. На сегодняшнем занятии мы начнём знакомиться с некоторыми итераторами, доступными классу Array
. Рассмотреть все у нас не хватит с одной стороны времени, а с другой стороны некоторые нам сейчас просто не нужны.
Итератор #each
Данный итератор предназначен для обхода всех элементов массива. Он эффективно заменяет цикл for
, а также является основой для построения более сложных методов обработки наборов данных.
Код, приставленный ниже, выводит стандартный вывод строку a -- b -- c --
a = [ "a", "b", "c" ]
a.each {|x| print x, " -- " }
Итератор #reverse_each
Данный итератор позволяет пройти по элементам массива в обратном порядке. Не требует предварительного разворота массива для подобного обхода.
a = [ "a", "b", "c" ]
a.reverse_each {|x| print x, " " } #=> c b a
Итератор #each_with_index
Данный итератор позволяет получить доступ одновременно к значению элемена и его порядковому номеру. Плюс по сравнению с for
- никогда не выйдете за границы массива. Код ниже выводит строку 0: a -- 1: b -- 2: c --
a = [ "a", "b", "c" ]
a.each_with_index {|x, index| print "#{index}: #{x} -- " }
Итератор #reduce
или inject
Данный итератор предназначен для применения некоторой общей операции над всеми элементами массива. Большинство математических операторов, обладающих свойством коммуникативности, можно легко применить с помощью данного итератора.
Например, для сложения всех элементов массива достаточно вызвать итератор следующим образом:
[1, 2, 3, 4].reduce(0) { |sum, n| sum + n }
Блок, ассоциированный с методом будет получать 2 аргумента: промежуточный результат и очередное значение из массива. Блок должен использовать эти 2 аргумента и на их основании формировать новый промежуточный результат, который будет использован при следующей итерации.
В качестве аргумента данному методу можно передать начальное значение, которое необходимо использовать на первой итерации. В примере передаётся число 0, которое будет использовано на первой итерации. Если данное число не передавать, то первоначальным значнеием промежуточного результата станет первый элемент массива, а обход массива начнётся со 2 элемента.
Итератор #delete_if
Итератор позволяет удалить элемент из массива, если данный элемент удовлетворяет некоторому условию. Условие описывает программист в блоке, который ассоциируется с данным массивом. Если блок возвращает true
, то данный элемент будет удалён.
Метод вернёт массив, состоящий из удалённых элементов или nil
, если ничего не было удалено.
Альтернативное название для данного метода - reject!
.
scores = [ 97, 42, 75 ]
scores.delete_if {|score| score < 80 }
puts scores #=> [97]
Итератор #bsearch
Итератор позволяет находить элементы с использованием бинарного поиска. Элементы массива должны быть отсортированы, чтобы поиск срабатывал. В противном случае поиск не будет работать.
Блок должен возвращать правду или ложь:
- false, если искомый элемент находится ближе к началу массива
- true, если искомый элемент находится ближе к концу массива
ary = [0, 4, 7, 10, 12]
ary.bsearch {|x| x >= 4 } #=> 4
Итератор #select
или #filter
Итератор позволяет создать новый массив путём отбрасывания элементов текущего массива, которые подходят под условие, описанное в блоке. Блок принимает 1 аргумент - элемент массива. Блок должен вернуть true, если элемент необходимо оставить, false, если необходимо убрать из массива.
[1,2,3,4,5].select {|num| num.even? } #=> [2, 4]
Итератор #map
или #collect
Итератор позволяет создать новый массив путём модификации элементов текущего массива. Блоку передаётся элемент массива, на основании которого необходимо сформировать новое значение. Все созданные значения помещаются в новый массив.
chars = [ "a", "b", "c", "d" ]
chars.map { |character| character + "!" } #=> ["a!", "b!", "c!", "d!"]
numbers = [1, 2, 4, 5]
numbers.map { |number| number ** 3 } #=> [1, 8, 64, 125]
Итераторы примеси Enumerable
Методы данной примеси доступны классам Array и Hash. Данная примесь предоставляет огромное количество методов, которые позволяют обрабатывать последовательности данных. Пожалуйста обратитесь к официальной документации, чтобы разобраться в назначении каждого из этих итераторов.
Задачи на изучение циклов и итераторов
Задачи необходимо решить двумя способами. В первом способе вам предлагается использовать итераторы для решения поставленной задачи. Затем вы должны скопировать работающую программу и перейти на использование обычных циклов в ключевой логике по работе с массивами. Сравните решения между собой.
При разработке приложений придерживайтесь следующих правил:
- Разрабатывайте приложение как набор методов, вызывающих друг друга.
- Выделите методы, ответственные за ввод данных от пользователя, и методы, ответственные за обработку данных. Таким образом можно будет легко выполнить требование, указанное в начале раздела.
- Ввод данных должен осуществляться со стандартного потока ввода.
- Приложение должно печатать информационное сообщение о своей работе, а также приветствие при вводе данных.
- Если человек ошибся при вводе данных, то приложение должно ему сообщать об ошибке и просить повторить данную процедуру.
Технические рекомендации:
- Проверяйте исходный код с помощью Rubocop.
- Для ввода данных со стандартного потока ввода рекомендуется использовать библиотеку
tty-prompt
- Для управления списком зависимостей ваших приложений используйте Bundler.
Решите следующие задачи на работу с наборами данных:
- Задача №2 из пункта 1.3 задачника (стр. 15).
- Задача №3 из пункта 1.3 задачника (стр. 16).
- Задача №15 из пункта 1.3 задачника (стр. 16).
- Задача №4 из пункта 1.3 задачника (стр. 16).