Условные операторы в Ruby: if, elsif, unless, case и тернарный оператор

Условные операторы в Ruby: if, elsif, unless, case и тернарный оператор Полезное

Условный оператор в Ruby — это конструкция, которая выполняет разные ветки кода в зависимости от того, истинно ли выражение. В Ruby их несколько: if/elsif/else, unless, модификаторы в конце строки, тернарный оператор ?: и case/when.

Две особенности Ruby отличают его от многих языков. Первая: if и case — выражения, они возвращают значение. Вторая: ложными считаются только nil и false, а 0, пустая строка и пустой массив — истинны. Ниже разберем каждую форму на запускаемых примерах и типичные ошибки новичков.

Минимальный пример: if, elsif, else

Начнем с полной программы, которую можно сохранить в файл ticket.rb и запустить командой ruby ticket.rb:

def ticket_price(age)
  if age < 7
    0
  elsif age < 18
    250
  else
    500
  end
end

[5, 12, 30].each do |age|
  puts "Возраст #{age}: #{ticket_price(age)} руб."
end

В консоли появятся три строки:

Возраст 5: 0 руб.
Возраст 12: 250 руб.
Возраст 30: 500 руб.

Разбор по шагам:

  1. if age < 7 проверяет первое условие. Если оно истинно, выполняется ветка до ближайшего elsif/else, остальные пропускаются.
  2. elsif age < 18 проверяется, только если все условия выше оказались ложными. Веток elsif может быть сколько угодно, порядок важен: сработает первая истинная.
  3. else выполняется, если не сработала ни одна ветка. Он необязателен.
  4. end закрывает всю конструкцию — один на весь if, независимо от числа elsif.

Метод ticket_price не содержит return: в Ruby метод возвращает значение последнего вычисленного выражения, а if целиком и есть это выражение. Для age = 12 значение if равно 250.

Что в Ruby считается истиной и ложью

Условие в Ruby не обязано быть строго true или false. Проверяется любое значение, и правило одно: ложны только nil и false, все остальное истинно.

[nil, false, 0, "", [], "false"].each do |v|
  puts "#{v.inspect} -> #{v ? 'истина' : 'ложь'}"
end
nil -> ложь
false -> ложь
0 -> истина
"" -> истина
[] -> истина
"false" -> истина
Значение Ruby Python JavaScript
0 истина ложь ложь
"" (пустая строка) истина ложь ложь
[] (пустой массив) истина ложь истина
nil / None / null ложь ложь ложь

Эта разница — частый источник ошибок у тех, кто пришел из Python или JavaScript.

Неверно — проверять «есть ли что-то в массиве» самим значением или длиной:

items = []
if items.length
  puts "Есть товары"
end

Результат: строка «Есть товары» печатается, хотя корзина пуста, потому что items.length равно 0, а 0 в Ruby истинен.

Исправление — спрашивать явно через методы-предикаты: items.empty?, items.any?, count.zero?, value.nil?.

items = []
if items.empty?
  puts "Корзина пуста"
end
# => Корзина пуста

if как выражение и однострочная запись

Раз if возвращает значение, его можно присвоить переменной. Если ни одна ветка не сработала и else нет, результатом будет nil.

temp = 25
status = if temp > 30 then "жарко" else "комфортно" end
puts status        # => комфортно

x = if false then 1 end
p x                # => nil

Ключевое слово then нужно, когда условие и ветка стоят на одной строке. В многострочной записи его обычно не пишут.

unless: условие с отрицанием

unless выполняет ветку, когда условие ложно. Это то же самое, что if !условие, но читается ближе к обычной речи.

balance = 0
puts "Нужно пополнить счет" unless balance.positive?
# => Нужно пополнить счет

У unless есть ветка else, но нет elsif. Практическое правило, которого придерживается большинство Ruby-стайлгайдов: unless хорош для одного простого условия без else. Двойное отрицание вроде unless !ready или unless ... else проще переписать через if.

Модификаторы: условие в конце строки

Короткую ветку из одной инструкции можно записать с условием после нее:

temp = 25
puts "Тепло" if temp > 20        # => Тепло
puts "Не мороз" unless temp < 0  # => Не мороз

Чаще всего модификаторы используют для guard clause — раннего выхода из метода, чтобы основная логика не пряталась в глубокой вложенности:

def discount(user)
  return 0 if user.nil?
  return 0 unless user[:active]

  user[:orders] > 10 ? 15 : 5
end

p discount(nil)                          # => 0
p discount({ active: false, orders: 20 }) # => 0
p discount({ active: true, orders: 20 })  # => 15
p discount({ active: true, orders: 3 })   # => 5

Граница приема: модификатор подходит для одной короткой инструкции. Если ветка длиннее строки или в ней несколько действий, лучше обычный if ... end.

Тернарный оператор

Тернарный оператор условие ? значение_если_истина : значение_если_ложь — компактная запись if/else для выбора одного из двух значений:

temp = 25
label = temp >= 18 ? "без куртки" : "в куртке"
puts label   # => без куртки

Он удобен внутри выражений и интерполяции строк. Вложенные тернарные операторы (a ? b : c ? d : e) читаются плохо — для трех и более вариантов берите if/elsif или case.

case/when: выбор из многих вариантов

case сравнивает одно значение с набором вариантов. Сравнение в when идет не через ==, а через оператор === (case equality), поведение которого определяет класс слева. Поэтому в when можно писать диапазоны, классы и регулярные выражения:

def grade(score)
  case score
  when 90..100 then "отлично"
  when 75...90 then "хорошо"
  when 60...75 then "удовлетворительно"
  when Integer then "неудовлетворительно"
  else "некорректный балл"
  end
end

[95, 90, 74, 10, "abc"].each { |s| puts "#{s.inspect}: #{grade(s)}" }
95: отлично
90: отлично
74: удовлетворительно
10: неудовлетворительно
"abc": некорректный балл

Что здесь происходит:

  • 90..100 — диапазон с включенной правой границей, 75...90 — без нее, поэтому 90 попадает в «отлично», а не в «хорошо». Скобки вокруг диапазона в when не нужны.
  • when Integer срабатывает для любого целого числа: Integer === 10 возвращает true.
  • Строка "abc" не подошла ни к одному варианту и ушла в else.
Что стоит в when Как работает === Пример
Значение равенство when "admin"
Диапазон входит ли в диапазон when 1..5
Класс является ли экземпляром класса when String
Регулярное выражение совпадает ли строка с шаблоном when /\A\d{6}\z/
Несколько значений через запятую совпадение с любым из них when Integer, Float

В отличие от switch в C или JavaScript, в Ruby нет «проваливания» в следующую ветку: выполняется только первая подходящая when, break не нужен. case без значения после него (case и сразу when условие) работает как цепочка if/elsif.

В Ruby 3.x есть еще форма case/in — сопоставление с образцом (pattern matching) для разбора хешей и массивов. Это отдельная тема, здесь ограничимся примером:

case { name: "Ann", age: 20 }
in { name: String => name, age: Integer => age } if age >= 18
  puts "#{name}: совершеннолетняя"
end
# => Ann: совершеннолетняя

Как выбрать конструкцию

Задача Что взять
Одно условие, одна короткая инструкция модификатор if/unless
Ранний выход из метода return ... if ... (guard clause)
Выбрать одно из двух значений тернарный оператор
Две-три ветки с разными условиями if/elsif/else
Одно значение против многих вариантов, диапазонов, классов case/when
Разбор структуры хеша или массива (Ruby 3.x) case/in

Частые ошибки

else if вместо elsif. Код else if x > 1 открывает новый вложенный if, которому нужен свой end. С одним end Ruby завершится синтаксической ошибкой (в Ruby 2.6 текст такой: syntax error, unexpected end-of-input, expecting end, в новых версиях формулировка другая). Исправление — писать elsif одним словом.

= вместо == в условии. if name = "Python" не сравнивает, а присваивает; результат присваивания — строка, она истинна, и ветка выполняется всегда. Ruby при этом печатает предупреждение found '= literal' in conditional, should be ==. Исправление — if name == "Python".

and/or вместо &&/|| в присваивании. У слов and и or приоритет ниже, чем у =:

ok = true and false
p ok   # => true
ok = true && false
p ok   # => false

В первой строке сначала выполнилось ok = true, а and false вычислилось отдельно. В условиях используйте &&, || и !.

Выводы

  • В Ruby ложны только nil и false; 0, "" и [] истинны, поэтому пустоту проверяют методами empty?, zero?, nil?.
  • if и case — выражения: их результат можно присвоить или вернуть из метода без return.
  • elsif пишется одним словом, unless лучше использовать без else, модификаторы — для одной короткой инструкции и guard clause.
  • case/when сравнивает через ===, поэтому принимает диапазоны, классы и регулярные выражения, и не проваливается в следующую ветку.

Где применяется / связь с практикой

Ветвления есть в любой программе: валидация ввода, расчет цены, маршрутизация запроса, обработка ошибок. Логика, которую вы отработали на Ruby, — истинность условия, порядок проверок, ранний выход, выбор между if и case — переносится на другие языки почти без изменений, меняется в основном синтаксис и правило истинности.

Освойте тему на практике

Отдельного курса по Ruby у Otus сейчас нет. Если цель — войти в разработку с нуля, удобный старт — Python: близкая по духу выразительность и широкий спрос. Разобрать условия, циклы и функции с наставником можно на курсе Python Developer. Basic, а познакомиться с преподавателями и форматом — на бесплатных открытых уроках Otus.

FAQ

Есть ли в Ruby оператор switch?
Ключевого слова switch нет, его роль выполняет case/when. Сравнение в нем идет через ===, а break между ветками не нужен.

Можно ли писать условие в скобках, как в C?
Можно: if (x > 5) работает, но в Ruby-коде скобки вокруг условия обычно опускают. Они полезны, когда нужно явно сгруппировать сложное выражение.

Чем nil? отличается от проверки if value?
if value ложно и для nil, и для false. value.nil? истинно только для nil, поэтому различает «значения нет» и «значение равно false».

OTUS Журнал