Перейти к основному содержимому

Управляющие конструкции и циклы в Ruby

Разработчику Архитектору

О чём эта статья

Операторы, if / unless, case с in, циклы и итераторы (each, map). В продакшене for по индексу почти не используют.

Практика: повторяйте примеры в IRB (первая программа).

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Три задачи управления потоком

ВопросКонструкции
Выполнять ли действие?if / unless
Какую ветку выбрать?case / when / in
Сколько раз повторять?each, times, while, loop

Идиоматичный пример без счётчика по индексу:

numbers = [1, 2, 3, 4, 5]
evens = numbers.select(&:even?).map { |n| n * 10 }
puts evens.inspect
  • select(&:even?) — только чётные элементы.
  • map { |n| n * 10 } — новый массив с умножением.
  • Цепочка читается как "возьми числа, отфильтруй, преобразуй".

Операторы и управление потоком

В Ruby операторы вроде + или == — это вызовы методов у объектов (1.+(2)). Условия: if / unless / case; повторения — while, until, loop, а для коллекций в идиоматичном коде — each, map, select, а не for по индексу.


1. Операторы

Play ITЗагрузка интерактивного демо…

В Ruby оператор — это синтаксическая оболочка для вызова метода объекта. Например, выражение a + b интерпретируется как вызов метода + у объекта a с аргументом b: a.+(b). Эта особенность позволяет переопределять поведение операторов в пользовательских классах через переопределение соответствующих методов.


1.1. Арифметические операторы

Арифметические операторы применяются к числовым объектам (Integer, Float, Rational, Complex) и возвращают новый объект соответствующего типа.

ОператорОписаниеПримерЭквивалентный вызов
+Сложение5 + 385.+(3)
-Вычитание5 - 325.-(3)
*Умножение4 * 284.*(2)
/Деление7 / 23 (целочисленное деление для Integer)7./(2)
%Остаток от деления (по модулю)7 % 317.%(3) или 7.modulo(3)
**Возведение в степень2 ** 382.**(3)

Важные особенности:

  • Для целых чисел (Integer) операция / выполняет целочисленное деление с усечением к нулю (не к минус бесконечности, в отличие от математического деления по модулю). Например, -7 / 3-2.
  • Оператор % в Ruby реализует модульное арифметическое значение, и его поведение определяется методом modulo. В отличие от языков вроде C или Java, результат % в Ruby всегда имеет тот же знак, что и делитель. Например:
    • 7 % 31
    • 7 % -3-2
    • -7 % 32
    • -7 % -3-1
      Это соответствует определению a.modulo(b) = a - b * (a.div(b)), где div — целочисленное деление с округлением вниз (floor division).
  • Оператор ** может принимать дробные и отрицательные показатели степени, возвращая Float при нецелом результате: 4 ** 0.52.0, 2 ** -10.5.

Переполнение целых чисел в Ruby не приводит к ошибке: Integer имеет неограниченную точность (arbitrary-precision integer, как BigInt в других языках). Например, 2 ** 1000 вычисляется без потери точности.


1.2. Операторы сравнения

Операторы сравнения возвращают объекты класса TrueClass, FalseClass или nil. В Ruby только два значения считаются ложными в булевом контексте — false и nil; всё остальное (включая 0, "", []) — истинное.

ОператорОписаниеПример
==Равенство значений5 == 5.0true
!=Неравенство3 != 4true
<, >, <=, >=Упорядоченное сравнение"apple" < "banana"true
<=>Оператор "космический корабль" (spaceship) — трёхстороннее сравнение5 <=> 31, 3 <=> 5-1, 4 <=> 4.00, 1 <=> "1"nil
===Оператор "case equality" — используется в case-выражениях, семантика зависит от левого операндаInteger === 42true, /a/ === "bar"true, Range === 5 — для 1..10 === 5true

Пояснения:

  • Оператор == вызывает метод ==, который по умолчанию проверяет равенство объектов по значению (а не по ссылке, в отличие от equal?). Он может быть переопределён — например, в String учитывает содержимое, регистр и кодировку.
  • Оператор <=> должен возвращать -1, 0, 1 или nil. Если сравнение бессмысленно (например, число и строка), возвращается nil. При реализации пользовательских классов определение <=> позволяет легко подключить миксин Comparable, который автоматически предоставляет <, <=, >, >= и between?.
  • Оператор === не симметричен. Его поведение зависит от класса левого операнда:
    • Для классов (Class): Class === obj эквивалентен obj.is_a?(Class).
    • Для диапазонов (Range): range === value эквивалентен range.cover?(value).
    • Для регулярных выражений (Regexp): /pattern/ === str эквивалентен str =~ /pattern/ (возвращает true, если совпадение найдено).
    • Для простых значений (String, Symbol, Integer и др.) поведение совпадает с ==, но это соглашение, а не требование.

1.3. Логические операторы

Ruby предоставляет два уровня логических операторов — низкоприоритетные (and, or, not) и высокоприоритетные (&&, ||, !). Разница — в приоритете относительно других операторов (в первую очередь =).

ОператорПриоритетАналог в других языкахОсобенности
&&Высокий&&Чаще используется в выражениях
``Высокий
!Высокий!Унарный оператор логического отрицания
andНизкийРедко используется; иногда применяется для потокового управления (например, do_something and return)
orНизкийАналогично and
notНизкийУнарный, редко используется: not truefalse

Короткое замыкание (short-circuit evaluation):

  • В a && b: если a ложно (false или nil), b не вычисляется.
  • В a || b: если a истинно (не false и не nil), b не вычисляется.

Примеры:

x = nil
y = x && x.length # → nil (x ложно, x.length не вызывается)
z = x || "default" # → "default"
w = false || true # → true

Разбор:

  • && и || работают с коротким замыканием и могут не вычислять правую часть.
  • x && x.length безопасен при nil, потому что x.length не вызывается.
  • x || "default" задаёт fallback-значение, если левый операнд ложный.
  • В Ruby такие выражения часто возвращают один из операндов, а не только true/false.

Поскольку в Ruby только false и nil ложны, логические операторы часто используются для установки значений по умолчанию или защиты от nil.


1.4. Операторы присваивания

Ruby поддерживает как простое присваивание, так и составные формы.

  • = — простое присваивание. Создаёт локальную переменную (если её нет), не вызывает метод.
    Пример: x = 10. Локальная переменная создаётся в момент анализа кода, даже если присваивание не выполнится (например, в ветке if false).

  • +=, -=, *=, /=, %=, **= — составные операторы присваивания.
    Выражение a += b эквивалентно a = a + b, то есть вызывает метод + у a, затем присваивает результат переменной a.
    Важно: это не модификация объекта на месте (если только + не реализован как +=), а создание нового объекта и перепривязка переменной.

  • ||= — оператор условного присваивания ("nil-coalescing assignment").
    x ||= y эквивалентно x = x || y, но с семантикой: присвоить y, только если x ложно (nil или false).
    Часто используется для кэширования:

def expensive_value
@expensive_value ||= compute_expensive_value
end

Разбор:

  • ||= реализует ленивую инициализацию: значение считается только один раз.

  • При повторных вызовах используется уже сохранённый результат.

  • Это стандартный шаблон мемоизации для тяжёлых вычислений в объекте.

  • &&= — аналогично: x &&= yx = x && y. Присваивает y, только если x истинно.

  • = в контексте параллельного присваивания:

a, b = 1, 2 # → a = 1, b = 2
a, b = [1, 2] # → a = 1, b = 2 (распаковка массива)
a, *rest = [1, 2, 3] # → a = 1, rest = [2, 3]

Разбор:

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

Особенность: в Ruby нет операторов инкремента/декремента (++, --). Вместо них используются += 1 или методы succ/next (для Integer, String и др.):
x = 5; x = x.succ6; "a".succ"b".


1.5. Побитовые операторы

Доступны для целых чисел. Все побитовые операторы — методы класса Integer.

ОператорОписаниеПример
&Побитовое И5 & 31 (0b101 & 0b011 = 0b001)
|Побитовое ИЛИ5 | 37 (0b101 | 0b011 = 0b111)
^Побитовое исключающее ИЛИ5 ^ 36
~Побитовое НЕ (унарный)~5-6 (дополнение до -1 по правилам двух’s complement)
<<Арифметический сдвиг влево2 << 316 (умножение на 2³)
>>Арифметический сдвиг вправо16 >> 24

Замечание: сдвиги работают с отрицательными числами корректно: -8 >> 1-4.


1.6. Специальные операторы

  • .. и ... — операторы диапазонов:

    • a..b — включает b ("замкнутый" диапазон),
    • a...b — исключает b ("полуоткрытый" диапазон). Оба создают объекты класса Range. Пример: (1..5).to_a[1, 2, 3, 4, 5]; (1...5).to_a[1, 2, 3, 4].
  • ? : — тернарный условный оператор:
    условие ? значение_если_истина : значение_если_ложь.
    Пример: x > 0 ? "positive" : "non-positive".

  • defined? — унарный оператор (на самом деле — метод верхнего уровня), возвращающий строку с типом выражения или nil, если выражение не определено:

defined?(x) # → nil (если x не определена)
x = 1
defined?(x) # → "local-variable"
defined?(Math::PI) # → "constant"
defined?(1 + 1) # → "expression"

Разбор:

  • defined? проверяет, существует ли сущность, без прямого обращения к ней.

  • Для неизвестного имени возвращается nil, а для известных — строка-категория.

  • Инструмент полезен в метапрограммировании и защитных проверках среды.

  • => — используется в хэш-литералах (до Ruby 1.9) и в case/when, но не является самостоятельным оператором в выражениях.


2. Условные конструкции

2.1. if, elsif, else

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

Синтаксис:

if условие1
# блок 1
elsif условие2
# блок 2
else
# блок else
end

Разбор:

  • if/elsif/else организует многошаговое ветвление по условиям.
  • Выполняется только первая подходящая ветка.
  • В Ruby это выражение, поэтому конструкция возвращает значение.
  • Блок завершается словом end, фигурные скобки не требуются.

Особенности:

  • Условие может быть любым объектом. Ложными считаются только false и nil.
  • Отсутствие фигурных скобок: блоки определяются ключевыми словами и отступами (по соглашению).
  • Возврат значения:
result = if x > 0
"positive"
elsif x < 0
"negative"
else
"zero"
end

Разбор:

  • В переменную result записывается значение из выбранной ветки.
  • Конструкция показывает, что if может использоваться как полноценное выражение.
  • Такой стиль уменьшает дублирование присваивания и делает код компактнее.

Модификаторы if и unless (постфиксная форма):

puts "x положительное" if x > 0
raise "ошибка" unless valid?

Разбор:

  • Постфиксный if удобен для коротких действий при выполнении условия.
  • unless читается как отрицательная проверка и часто применяется в guard-выражениях.
  • Такой формат лучше оставлять для простых строк, чтобы не снижать читаемость.

Удобны для однострочных проверок. Приоритет ниже, чем у присваивания:
x = y if condition(x = y) if condition, а не x = (y if condition).


2.2. unless

Эквивалент if not, но читается естественнее при формулировке исключений:

unless user.admin?
redirect_to root_path
end

Разбор:

  • unless выполняет блок, когда условие возвращает false или nil.
  • Пример показывает простую проверку прав перед действием.
  • Это типичный приём раннего отклонения запроса в веб-коде.

Поддерживает else, но не поддерживает elsif (для цепочек лучше использовать if).


2.3. case

Многоальтернативная условная конструкция. Имеет две формы.

Форма 1: без аргумента у case — как if/elsif:

case
when x < 0
"отрицательное"
when x == 0
"ноль"
else
"положительное"
end

Разбор:

  • case без аргумента проверяет логические условия в ветках when.
  • Срабатывает первая истинная ветка, остальные пропускаются.
  • Конструкция возвращает строку из выбранной ветки как результат выражения.

Форма 2: с аргументом у case — использует оператор === для сравнения:

case x
when Integer
"целое"
when String
"строка"
when 1..10
"от 1 до 10"
when /foo/
"содержит 'foo'"
else
"неизвестно"
end

Разбор:

  • Ветки сравниваются через ===, а не через обычный ==.
  • Благодаря этому в when работают классы, диапазоны и регулярные выражения.
  • Такой формат особенно удобен для классификации входных данных по типам и шаблонам.

Здесь Integer === x, String === x, (1..10) === x, /foo/ === x — именно поэтому === так важен.

Возврат значения: как и if, case возвращает значение последнего выражения в сработавшей ветке.


2.4. Pattern matching (case / in) — Ruby 2.7+, зрелый с 3.x

Отдельно от классического case/when с оператором === в Ruby 2.7 появился pattern matching — ветки записываются через in, а слева сопоставляется структура данных (хэш, массив, объект), а не одно значение через ===.

response = { status: 200, body: '{"ok":true}' }

message = case response
in { status: 200, body: }
"OK: #{body}"
in { status: 404 }
"Not found"
in { status: 500..599 }
"Server error"
else
"Unexpected"
end

Разбор:

  • case ... in выполняет структурное сопоставление шаблонов с данными.
  • Шаблон in { status: 200, body: } одновременно проверяет статус и извлекает body.
  • Диапазон 500..599 в шаблоне помогает лаконично покрыть класс серверных ошибок.
  • else закрывает все неожиданные структуры входных данных.

Деструктуризация — ключи и поля извлекаются прямо в шаблоне:

case { role: "admin", name: "Ada" }
in { role: "admin", name: }
puts "Hello, #{name}"
in { role: }
puts "Access denied"
end

Закрепление значения (pin) — оператор ^ запрещает переприсвоение уже известной переменной:

expected = 200
case { status: 200 }
in { status: ^expected }
"status matches"
else
"mismatch"
end

Массивы и вложенность:

case [1, 2, 3]
in [first, second, *rest]
puts "#{first}, #{second}, rest=#{rest.inspect}"
end

Разбор:

  • Шаблон массива раскладывает данные по позициям в переменные first и second.
  • *rest собирает остаток элементов в отдельный массив.
  • Такой формат удобен при обработке структурированных сообщений переменной длины.

Когда что выбирать:

ЗадачаКонструкция
Класс, диапазон, regexp, ===case x + when
Разбор JSON/API-ответа, вложенные хэшиcase + in
Простые ветки по одному значениюif / elsif или case без in

Pattern matching дополняет, но не заменяет when: для Integer === x по-прежнему удобен классический case. История появления — в Истории Ruby.


Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В Ruby синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…


3. Циклические конструкции

В Ruby существует несколько способов организации повторяющихся вычислений — традиционные управляющие конструкции (while, until, loop), а также итераторы — методы, принимающие блоки кода. Последние составляют основу идиоматического стиля Ruby и предпочтительны в большинстве случаев, поскольку обеспечивают лучшую читаемость, инкапсуляцию логики и устойчивость к ошибкам (например, забытому изменению счётчика).


3.1. while и until

Конструкции while и until синтаксически схожи и отличаются только условием продолжения.

Синтаксис while:

while условие
# тело цикла
end

Разбор:

  • while проверяет условие перед каждой итерацией.
  • Тело выполняется только пока условие остаётся истинным.
  • При ложном стартовом условии цикл не выполнится ни разу.

Цикл выполняется, пока условие истинно (не false и не nil). Проверка условия происходит перед каждой итерацией. Если условие изначально ложно, тело цикла не выполнится ни разу.

Синтаксис until:

until условие
# тело цикла
end

Цикл выполняется, пока условие ложно. Эквивалентно while !(условие), но выразительнее в случаях вроде "пока пользователь не ввёл корректные данные".

Модификаторы while и until (постфиксная форма):

x = 0
x += 1 while x < 10

Обратите внимание — в постфиксной форме тело цикла выполняется хотя бы один раз, даже если условие изначально ложно, — но только если тело представляет собой одно выражение. В случае составного тела (через begin/end) поведение меняется.

Особый случай: begin/end while

begin
puts "Введите число:"
input = gets.chomp
end while input.empty?

Такая форма гарантирует, что тело выполнится минимум один раз — аналог do/while в C-подобных языках. Однако в Ruby 3.0+ такая конструкция считается устаревшей и вызывает предупреждение (warning: (...) while (...) without rescue is a syntax error в будущем), поэтому предпочтительно использовать loop + break.


3.2. loop

Бесконечный цикл, прерываемый явно через break, return или exit:

loop do
# тело
break if условие_выхода
end

Разбор:

  • loop do создаёт бесконечный цикл.
  • Выход определяется явно через break, что упрощает чтение сложной логики.
  • Такой шаблон полезен для циклов с несколькими сценариями завершения.

Конструкция loop — единственная в Ruby, гарантированно реализованная как встроенная (не метод), что обеспечивает максимальную производительность. Она часто используется как основа для пользовательских циклов с нетривиальной логикой выхода.

Преимущества loop:

  • Чётко выражает намерение: "повторять бесконечно, пока не будет явного выхода";
  • Позволяет использовать break с возвратом значения:
result = loop do
input = gets.chomp
break input unless input.empty?
end

3.3. Итераторы — основа циклических операций в Ruby

В Ruby итерация реализована через методы-итераторы, которые принимают блоки (block) — анонимные функции, ограниченные по времени жизни вызовом итератора. Эта модель позволяет абстрагироваться от деталей управления счётчиком, граничными условиями и инкрементом.


3.3.1. Integer#times

Повторяет блок заданное количество раз. Передаёт в блок текущий индекс (от 0 до n-1):

5.times { |i| puts "Итерация #{i}" }
# → 0, 1, 2, 3, 4

Эквивалентно for i in 0...5, но без создания переменной цикла в окружающей области видимости.


3.3.2. Integer#downto и Integer#upto

Генерируют последовательность целых чисел в указанном диапазоне:

5.downto(1) { |i| puts i } # → 5, 4, 3, 2, 1
1.upto(5) { |i| puts i } # → 1, 2, 3, 4, 5

Работают и с нецелыми числами, но только если шаг равен 1 (иначе — ошибка). Для дробных шагов используются другие подходы.


3.3.3. Range#each

Перебирает все элементы диапазона. Реализован для числовых и символьных диапазонов:

('a'..'e').each { |c| print c } # → abcde
(1..3).each { |n| puts n } # → 1, 2, 3

Важно: для больших или бесконечных диапазонов (1..Float::INFINITY) вызов each приведёт к зависанию — в таких случаях используют lazy (ленивые вычисления).


3.3.4. Array#each, Hash#each и другие коллекции

Стандартные коллекции предоставляют метод each, который применяет блок к каждому элементу:

[1, 2, 3].each { |x| puts x * 2 } # → 2, 4, 6
{a: 1, b: 2}.each { |k, v| puts "#{k}=#{v}" } # → a=1, b=2

Поведение each определено протоколом Enumerable, который реализуется почти всеми стандартными коллекциями. При реализации пользовательских коллекций достаточно определить each, чтобы получить доступ к map, select, reduce и другим методам.


3.3.5. Контроль потока внутри блока — next, break, redo

Блоки поддерживают специальные ключевые слова для управления итерацией:

  • next — завершает текущую итерацию и переходит к следующей (аналог continue в C/Java). Может возвращать значение, передаваемое итератору:
(1..5).map { |x| next 0 if x.even?; x } # → [1, 0, 3, 0, 5]
  • break — прерывает итерацию и возвращает управление за пределы итератора. Может принимать значение, которое станет результатом всего выражения итератора:
result = [1, 2, 3, 4, 5].each { |x| break x * 10 if x > 3 }
# result → 40
  • redo — повторяет текущую итерацию с самого начала, не изменяя состояние итератора (например, не увеличивая счётчик). Опасен — может привести к бесконечному циклу при неосторожном использовании:
count = 0
3.times do |i|
count += 1
redo if count < 5
puts "i=#{i}, count=#{count}"
end
# Выведет три строки, но count будет ≥5 к моменту вывода

3.3.6. Ленивые итераторы (lazy)

Для работы с потенциально бесконечными или очень большими последовательностями Ruby предоставляет ленивые вычисления через Enumerator::Lazy:

result = (1..Float::INFINITY)
.lazy
.select(&:even?)
.map { |x| x ** 2 }
.first(5)
# → [4, 16, 36, 64, 100]

Здесь цепочка методов (select, map) не выполняется сразу, а откладывается до вызова first, который запрашивает ровно пять элементов. Это позволяет эффективно обрабатывать потоки данных без избыточного потребления памяти.


3.4. for — редкая конструкция в современном Ruby

Синтаксис:

for переменная in коллекция
# тело
end

Семантически эквивалентен collection.each { |переменная| ... }, но с важным отличием: переменная переменная создаётся в текущей области видимости, а не в локальной области блока. Это может привести к неожиданным эффектам перезаписи переменных.

Пример:

x = "внешняя"
for x in [1, 2, 3]
# ...
end
puts x # → 3 (переменная x перезаписана)

В современном Ruby конструкция for используется редко. Обычно выбирают each, потому что он безопаснее по области видимости и привычнее для команды.


4. Идиомы и лучшие практики

4.1. Предпочтение итераторов перед while/for

КонструкцияКогда уместна
each, map, select, reduceобход коллекций, трансформации (основной стиль)
times, upto, downto, Range#eachфиксированное число шагов или диапазон
while / untilусловие выхода заранее не сводится к "пройти коллекцию"
loop + breakбесконечный цикл с явным выходом
for x in arrпочти никогда в новом коде (см. § 3.4)

Итераторы:

  • исключают ошибки в управлении счётчиком ("на единицу больше/меньше");
  • обеспечивают локальность переменных;
  • позволяют легко комбинировать трансформации (map, select, reduce);
  • естественно интегрируются с функциональным стилем.

Пример "неправильного" цикла:

i = 0
result = []
while i < array.length
result << array[i] * 2 if array[i].even?
i += 1
end

Идиоматическая замена:

result = array.select(&:even?).map { |x| x * 2 }

4.2. Использование case вместо цепочек if/elsif

Когда проверяется одно и то же значение по нескольким условиям, case читается яснее и быстрее работает (благодаря возможной оптимизации через хэши или таблицы переходов в некоторых реализациях).


4.3. Избегание побочных эффектов в условиях

Условия должны быть чистыми — без изменения состояния. Например, избегайте:

if (x = get_value) && x > 0 # присваивание внутри условия

Лучше:

x = get_value
if x && x > 0

Это повышает предсказуемость и облегчает отладку.


4.4. nil-безопасность через &. (safe navigation operator)

Начиная с Ruby 2.3, оператор &. позволяет безопасно вызывать методы у потенциально nil-объектов:

user&.profile&.name
# эквивалентно —
user && user.profile && user.profile.name

Уменьшает многословность защитных проверок.


4.5. Частые сценарии из реальной разработки

Валидация входных данных

def build_username(input)
cleaned = input&.strip
return "guest" if cleaned.nil? || cleaned.empty?

cleaned.downcase
end

Разбор:

  • input&.strip удаляет пробелы и безопасно обрабатывает nil через &..
  • Guard-ветка return "guest" закрывает невалидный ввод максимально рано.
  • В последней строке остаётся только бизнес-действие: нормализация в нижний регистр.
  • Такой шаблон уменьшает вложенность и делает код легче в сопровождении.

Обработка API-ответа через case in

def parse_status(response)
case response
in { status: 200, body: body }
"ok: #{body}"
in { status: 404 }
"not_found"
else
"unexpected"
end
end

Разбор:

  • case in сопоставляет структуру хэша с шаблонами веток.
  • В ветке status: 200 значение body сразу извлекается в локальную переменную.
  • Код легко расширяется новыми статусами без длинной цепочки if/elsif.
  • Возвращаются короткие машинно-удобные строки, которые легко использовать дальше.

Эти примеры показывают, как Ruby помогает держать код компактным и читаемым без потери точности.


4.6. Дополнительные сниппеты

Ранний выход из цикла через break

result = [3, 7, 10, 15].each do |n|
break n if n.even?
end

puts result

Разбор:

  • each начинает перебор массива слева направо.
  • break n завершает итерацию при первом чётном числе и возвращает это значение.
  • В result попадёт 10, потому что это первое число, удовлетворяющее условию.
  • Сниппет показывает, что break может возвращать данные, а не только останавливать цикл.

Фильтрация и трансформация в одной цепочке

orders = [{ sum: 1200 }, { sum: 300 }, { sum: 800 }]
vip_totals = orders
.select { |o| o[:sum] >= 800 }
.map { |o| o[:sum] }

p vip_totals

Разбор:

  • select оставляет только заказы по условию суммы.
  • map преобразует объекты заказов в список чисел.
  • Цепочка выражает pipeline-логику без ручного состояния и промежуточных счётчиков.
  • Результат vip_totals будет [1200, 800].

5. Приоритеты операторов в Ruby

Понимание приоритета операторов критично для корректного чтения и написания выражений без избыточных скобок. В Ruby приоритеты строго фиксированы и не зависят от контекста.

Ниже приведён список операторов в порядке убывания приоритета (сверху — самый высокий):

ПриоритетОператорыПримечания
1::Разрешение области видимости (например, Math::PI)
2[], []=Доступ и присваивание по индексу/ключу
3**Возведение в степень (правоассоциативный: 2 ** 3 ** 2 = 2 ** 9)
4унарные +, -, !, ~+5, -x, !condition, ~mask
5*, /, %Арифметические: умножение, деление, модуль
6+, -Сложение и вычитание
7<<, >>Побитовые сдвиги
8&Побитовое И
9|, ^Побитовое ИЛИ, исключающее ИЛИ
10<, <=, >, >=, <=>, ==, ===, !=, =~, !~Операторы сравнения и сопоставления
11&&Логическое И (высокий приоритет)
12||Логическое ИЛИ (высокий приоритет)
13.., ...Диапазоны
14? :Тернарный условный оператор
15=, +=, -=, и др. составные присваиванияВсе формы присваивания (низкий приоритет)
16notЛогическое НЕ (низкоприоритетное)
17and, orЛогические И/ИЛИ (низкоприоритетные)
18defined?Проверка определённости
19if, unless, while, until (в постфиксной форме)Модификаторы — самый низкий приоритет

Важные следствия:

  • a = b && c интерпретируется как a = (b && c), а не (a = b) && c.
    Это означает: сначала вычисляется b && c, затем результат присваивается a.
  • a = b and c(a = b) and c. Здесь присваивание происходит первым, затем результат присваивания (значение b) участвует в and c.
  • x = y ? a : bx = (y ? a : b) — тернарный оператор выше присваивания.

Рекомендация: избегайте смешивания and/or с присваиванием без скобок. При сомнениях — используйте &&/|| или явные скобки.


6. Сравнительный анализ — Ruby и Python и JavaScript

АспектRubyPythonJavaScript
Операторы как методыДа: a + ba.+(b)Нет: + — встроенная операция (но __add__ для перегрузки)Нет: + — встроенный, перегрузка невозможна
Логическое "и"&& (высокий приоритет), and (низкий)and (единственный, приоритет ниже !=, выше not)&& (единственный)
Условное присваивание`=("nil-coalescing assignment"),&&=`
Нулевое слияниеНет оператора ??, но `xyчасто используется (результат —y, если x` ложно)
Циклы по коллекцииeach, map, select (итераторы + блоки)for item in list:, map(), filter()for-of, forEach, map, filter
Условие в whileЛюбое значение: только false/nil — ложныеЛюбое значение: 0, "", [], None — ложныеЛюбое значение: 0, "", null, undefined, NaN, false — falsy
Оператор "космический корабль"<=> (встроен, возвращает -1, 0, 1, nil)Нет (но cmp() в Python 2, или (a > b) - (a < b))Нет (но можно реализовать)
Диапазоны1..5, 1...5 — объекты Rangerange(1, 6) — генератор/последовательностьНет (только через массивы или for (let i = 1; i <= 5; i++))
Безопасная навигация&. (начиная с 2.3)Нет (но getattr(obj, 'attr', default))?. (ES2020)

Ключевое отличие Ruby: единообразие через объектность. Поскольку всё — объект, поведение операторов можно расширять. Например, можно определить Vector#* для скалярного умножения, и v * 2 будет работать интуитивно. В Python перегрузка тоже возможна, но через специальные методы (__mul__); в JavaScript — невозможно без изменения прототипов (что небезопасно).


7. Типичные ошибки и диагностика

7.1. Перепутывание and/or с &&/||

Ошибка:

user = find_user && user.active? # OK: сначала find_user, затем active?
user = find_user and user.active? # ОПАСНО: эквивалентно (user = find_user) and user.active?

Если find_user возвращает nil, вторая часть не выполнится — но если возвращает объект, user.active? вызовется и проигнорируется (результат не присваивается никуда).

Диагностика: RuboCop выдаёт предупреждение Style/AndOr при использовании and/or вне управляющих конструкций.


7.2. Изменение коллекции внутри each

Ошибка:

arr = [1, 2, 3, 4]
arr.each { |x| arr.delete(x) if x.even? }
# Результат — [1, 3] — но итерация пропустит элементы!

При удалении элементов индексы смещаются, и each "перепрыгивает" через следующий элемент.

Решение: использовать delete_if, reject!, select — методы, предназначенные для фильтрации:

arr.delete_if(&:even?)

7.3. Использование for и утечка переменной

Ошибка:

name = "глобальное"
for name in ["Алиса", "Боб"]
# ...
end
puts name # → "Боб"

Переменная name перезаписана.

Решение: заменить на each:

["Алиса", "Боб"].each { |name| ... } # name локальна в блоке

7.4. x ||= y и ложные, но значимые значения

Ошибка:

count ||= 10

Если count == 0, выражение присвоит 10, хотя 0 — валидное значение.

Решение: проверять явно на nil:

count = 10 if count.nil?
# или
count = defined?(count) && count.nil? ? 10 : count # избыточно
# лучший вариант — инициализировать при объявлении

7.5. Сравнение == и equal?

Ошибка:

a = "hello"
b = "hello"
a == b # → true
a.equal?(b) # → false (разные объекты)

equal? проверяет тождественность объектов (один и тот же object_id), а не равенство содержимого.

Когда использовать:

  • == — для семантического равенства,
  • equal? — для проверки, ссылается ли переменная на тот же самый объект (редко, в основном в метапрограммировании или тестах).

8. Практикум (необязательное приложение)

Для закрепления материала рекомендуются следующие упражнения. Каждое можно реализовать в виде отдельной задачи в обучающем курсе.


Упражнение 1. Реализация fizz_buzz тремя способами

  1. С while
  2. С loop + break
  3. С (1..n).each
    Сравните читаемость, длину кода, риск ошибок.

Упражнение 2. Безопасное вычисление цепочки методов

Дан объект:
user = { profile: { settings: { theme: "dark" } } }
Напишите выражение, возвращающее theme или "default", если любой уровень nil.
Решения:

  • С вложенными &&
  • С &. (safe navigation)
  • С dig (встроенный метод Hash#dig)

Упражнение 3. Перегрузка оператора

Создайте класс Vector2D с координатами x, y. Реализуйте:

  • + — покомпонентное сложение,
  • * — умножение на скаляр (если аргумент — число) и скалярное произведение (если — Vector2D),
  • <=> для сортировки по длине вектора.

Упражнение 4. Ленивая фильтрация бесконечной последовательности

Сгенерируйте первые 10 простых чисел, начиная с 2, с использованием lazy.


Связанные статьи энциклопедии