При промышленной разработке нам зачастую необходимо обрабатывать большие наборы данных. Для решения соответствующих задач на 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.

Модификаторы unti и 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

Данный цикл предназначен для прохождения по элементам массива. В настоящее время не используется, так как инструмент значительно слабее итераторов, мы не будем его рассматривать.

Бесконечный итератор 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.

Переход к следующей итерации с помощью 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, " -- " }

Итератор #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]

Задачи

Задачи, предлагаемые на этом занятии, необходимо решить двумя способами. В первом способе вам предлагается использовать итераторы для решения поставленной задачи. Затем вы должны скопировать работающую программу и перейти на использование обычных циклов в ключевой логике по работе с массивами. Сравните решения между собой.

При разработке приложений придерживайтесь следующих правил:

  • Разрабатывайте приложение как набор методов, вызывающих друг друга.
  • Выделите методы, ответственные за ввод данных от пользователя, и методы, ответственные за обработку данных. Таким образом можно будет легко выполнить требование, указанное в начале раздела.
  • Ввод данных должен осуществляться со стандартного потока ввода.
  • Приложение должно печатать информационное сообщение о своей работе, а также приветствие при вводе данных.
  • Если человек ошибся при вводе данных, то приложение должно ему сообщать об ошибке и просить повторить данную процедуру.

Решите следующие задачи с использованием итераторов:

  • Задача №2 из пункта 1.3 задачника (стр. 15).
  • Задача №3 из пункта 1.3 задачника (стр. 16).
  • Задача №15 из пункта 1.3 задачника (стр. 16).
  • Задача №4 из пункта 1.3 задачника (стр. 16).