How to prevent problems with `return` from block when using Ruby `yield`

时间:2016-12-12 12:28:53

标签: ruby yield

As every Ruby programmer eventually discovers, calling blocks or procs that contain return statements can be dangerous as this might exit your current context:

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"    

In cases you want to be absolutely sure some of your code runs after you called a block or proc, you can use a begin ... ensure construct. But since ensure is also called if there is an exception during yield, it requires a little more work.

I've created a tiny module中检索以两种不同方式处理此问题的“href”值:

  1. 使用safe_yield,检测生成的块或proc是否实际使用return关键字返回。如果是这样,它会引发异常。

    unknown_block = proc do
      return
    end 
    
    ReturnSafeYield.safe_yield(unknown_block)
    # => Raises a UnexpectedReturnException exception
    
  2. 使用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
    
  3. 我正在考虑用这个创建一个快速的Gem,还是有任何内置的解决方案来阻止我错过的嵌套块的快速返回?

1 个答案:

答案 0 :(得分:3)

有一个内置的解决方案来检测一个块是否包含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是因为它看起来很相关,而且因为(我认为!)没有可能导致LocalJumpError被提升的Ruby代码#34;自然而然地#34;在这种背景下。如果结果是假的,您可以轻松替换自己的新异常类。