Блоки, yield и передача блока

Блоки — визитная карточка Ruby. Это анонимные куски кода, которые вы передаёте методам, чтобы те «дорисовали» своё поведение. Без блоков не работает ни один итератор.
Суть: блок — это безымянный фрагмент кода в { } или do..end, передаваемый методу; внутри метода его запускают через yield, а block_given? проверяет, передали ли блок вообще.

Вы уже встречали блоки: [1,2,3].each { |n| puts n } — фигурные скобки и есть блок. Магия в том, что each не знает заранее, что вы хотите сделать с каждым элементом — вы сообщаете ему это блоком. Это инверсия управления: метод управляет перебором, а блок — действием.

def repeat(times)
  i = 0
  while i < times
    yield i        # запускаем переданный блок, отдавая ему i
    i += 1
  end
end

repeat(3) { |n| puts "повтор #{n}" }
# => повтор 0 / повтор 1 / повтор 2

Разбор: yield и block_given?

yield — это «вызвать переданный блок». Если блок ждёт аргументы, их передают yield значение. Чтобы метод не падал, когда блок не передали, есть проверка block_given?.

def with_logging
  puts "=== начало ==="
  result = block_given? ? yield : "блок не передан"
  puts "=== конец ==="
  result
end

with_logging { puts "делаю работу" }
# === начало === / делаю работу / === конец ===

Как работает под капотом

Блок — это «скрытый аргумент» метода. Он не указывается в списке параметров, но всегда доступен через yield. Когда исполнение доходит до yield, управление прыгает в тело блока, выполняет его и возвращается обратно в метод. Так строится поток each: метод и блок передают управление туда-сюда.

   repeat(3) { |n| puts n }
        |
   вход в метод repeat
        |
   yield 0  ----> прыжок в БЛОК, n=0, puts --> возврат
        |
   yield 1  ----> прыжок в БЛОК, n=1, puts --> возврат
        |
   yield 2  ----> прыжок в БЛОК, n=2, puts --> возврат
        |
   выход из метода

Та же идея «передать функции поведение» на Python — это передача функции-аргумента:

# Та же логика на Python ▶
def repeat(times, action):
    for n in range(times):
        action(n)            # вызываем переданное поведение

repeat(3, lambda n: print(f"повтор {n}"))
# повтор 0 / повтор 1 / повтор 2

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

  • yield без блока. Если метод вызвал yield, а блок не передали, будет LocalJumpError. Защищайтесь через block_given?.
  • Путать { } и do..end. Они почти эквивалентны, но различаются приоритетом: { } привязывается к ближайшему методу, do..end — слабее. На сложных цепочках это ловушка.
  • Возвращать из блока через return. return внутри блока выходит из объемлющего метода, а не из блока — частый сюрприз.

Best practices

  • Используйте { } для однострочных блоков и do..end для многострочных — это негласный стандарт.
  • Если метод может работать с блоком и без него — всегда проверяйте block_given?.
  • Блоки — идеальный инструмент для паттерна «обернуть действие»: открыть-сделать-закрыть (файлы, соединения, замеры времени).

Глубже: блоки как паттерн «ресурс под контролем»

Помимо итерации, у блоков есть вторая огромная область применения, которую стоит увидеть сразу: управление ресурсами по принципу «открыл — сделал — гарантированно закрыл». Классический пример — работа с файлом. Метод File.open с блоком сам открывает файл, передаёт его вам в блок, а после выхода из блока гарантированно закрывает — даже если внутри случилось исключение. Вам не нужно вручную помнить про закрытие, и ресурс не утечёт. Этот паттерн пронизывает весь язык и его библиотеки: соединения с базой данных, замеры времени, временные изменения настроек, транзакции — всё оформляется как «метод, принимающий блок». Вы передаёте методу действие, а он берёт на себя обвязку вокруг него. Когда вы начнёте писать собственные такие методы, структура будет одинаковой: подготовить ресурс, вызвать yield внутри begin/ensure, в ensure освободить ресурс. Освоив этот приём, вы получаете в руки мощнейший инструмент инкапсуляции «обвязочной» логики — и начинаете видеть его повсюду в чужом коде.

Итог. Блок — анонимный код, передаваемый методу как скрытый аргумент и запускаемый через yield. block_given? страхует от отсутствия блока, а { } и do..end отличаются приоритетом. Это фундамент всех итераторов Ruby.

Проверьте себя
1. Что делает ключевое слово yield внутри метода?
AВозвращает значение из метода
BЗапускает блок, переданный методу при вызове
CСоздаёт новый блок
DЗавершает программу
2. Зачем нужен block_given? перед вызовом yield?
AЧтобы ускорить блок
BЧтобы проверить, передали ли блок, и избежать LocalJumpError
CЧтобы создать блок автоматически
DЭто синоним yield