Как предотвратить проблемы с `return` из блока при использовании Ruby` yield`
Как только каждый программист Ruby обнаруживает, что вызовы или procs, содержащие инструкции return
, могут быть опасны, поскольку это может выйти из вашего текущего контекста:
def some_method(&_block)
puts 1
yield
# The following line will never be executed in this example
# as the yield is actually a `yield-and-return`.
puts 3
end
def test
some_method do
puts 2
return
end
end
test
# This prints "1\n2\n" instead of "1\n2\n3\n"
В случаях, когда вы хотите быть абсолютно уверены, что некоторые из ваших кодов запускаются после вызова блока или proc, вы можете использовать конструкцию begin ... ensure
. Но поскольку ensure
также вызывается, если во время урока есть исключение, требуется немного больше работы.
Я создал крошечный модуль, который рассматривает эту проблему двумя разными способами:
-
Используя safe_yield
, выясняется, действительно ли возвращаемый блок или proc возвращается с использованием ключевого слова return
. Если это так, возникает исключение.
unknown_block = proc do
return
end
ReturnSafeYield.safe_yield(unknown_block)
# => Raises a UnexpectedReturnException exception
-
Используя call_then_yield
, вы можете вызвать блок, а затем убедиться, что выполняется второй блок, даже если первый блок содержит оператор return
.
unknown_block = proc do
return
end
ReturnSafeYield.call_then_yield(unknown_block) do
# => This line is called even though the above block contains a `return`.
end
Я собираюсь создать быстрый Gem из этого или есть встроенное решение для предотвращения быстрого возврата из вложенного блока, который я пропустил?
Ответы
Ответ 1
Существует встроенное решение для определения того, содержит ли блок инструкцию return
.
Вы можете использовать RubyVM::InstructionSequence.disasm
, чтобы разобрать блок, переданный пользователем, затем найдите его для throw 1
, который представляет собой return
.
Здесь пример реализации:
def safe_yield(&block)
if RubyVM::InstructionSequence.disasm(block) =~ /^\d+ throw +1$/
raise LocalJumpError
end
block.call
end
Здесь вы можете включить его в свою библиотеку:
def library_method(&block)
safe_yield(&block)
puts "library_method succeeded"
rescue LocalJumpError
puts "library_method encountered illegal return but resumed execution"
end
И здесь пользовательский опыт для хорошо себя работающего и плохого пользователя:
def nice_user_method
library_method { 1 + 1 }
end
nice_user_method
# library_method succeeded
def naughty_user_method
library_method { return false if rand > 0.5 }
end
naughty_user_method
# library_method encountered illegal return but resumed execution
Комментарий:
Использование raise LocalJumpError
/rescue LocalJumpError
охватывает проблемы, с которыми вы столкнулись при использовании обложки ensure
.
Я выбрал LocalJumpError
, потому что это кажется актуальным, и потому что (я думаю!) нет никакого возможного кода Ruby, который приведет к тому, что LocalJumpError
будет "естественно" поднят в этом контексте. Если это окажется ложным, вы можете легко заменить свой собственный новый класс исключений.