Блоки, 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.