Ruby的一个方面是,它有throw 和catch 以及raise 和rescue 语句,这常常使来自其他语言的新手感到困惑。在这篇文章中,我将尝试澄清这个困惑。
如果你熟悉Java、C#、PHP或C++,你可能习惯于使用try 、catch 和throw 来处理异常。你使用try 来划分你预期可能发生异常的块。你使用catch 来指定当异常发生时应该做什么。你还可以使用throw 来自己引发异常。
你可能已经注意到Ruby有throw 和catch...但它们似乎并不像你在其他语言中习惯使用的那样!还有这些 "begin"、"raise"和"rescue"语句,似乎也在做同样的事情。这到底是怎么回事?
迅速脱身
如果你在另一种语言(如Java)中做过很多编程,你可能已经注意到,异常有时被用于非错误情况。"控制流的异常 "是一种技术,当开发者想从一个特定的执行路径中 "提前逃脱 "时,他们有时会求助于这种技术。
例如,想象一下一些代码,这些代码会搜刮一系列的网页,寻找包含一个特定文本字符串的网页。
def show_rank_for(target, query)
rank = nil
each_google_result_page(query, 6) do |page, page_index|
each_google_result(page) do |result, result_index|
if result.text.include?(target)
rank = (page_index * 10) + result_index
end
end
end
puts "#{target} is ranked #{rank} for search '#{query}'"
end
show_rank_for("avdi.org", "nonesuch")
(为了简洁起见,我排除了#each_google_result_page 和#each_google_result 方法的定义。你可以在gist.github.com/1075364 上查看完整的源代码)。
抓取网页和解析它们是很耗时的。如果目标文本是在第2页找到的呢?这段代码会一直进行下去,直到它达到最大的结果页数(这里指定为6)。
如果我们能在找到一个匹配的结果后立即结束搜索,那就更好了。我们可能会想到使用break 关键字,它可以 "跳出 "循环的执行。但是break 只会脱离紧邻的循环,而这里我们有一个循环在另一个循环里面。
在这种情况下,我们可能会想出用异常来突破两级循环的想法。但是,异常应该是针对意外的失败的,而找到我们要找的结果既不是意外的,也不是失败的!该怎么做呢?
向Ruby抛出一个快速球
Ruby给了我们一个工具来处理这种情况。与其他语言不同的是,Ruby的throw 和catch 不是用来处理异常的。相反,它们提供了一种方法,在不需要进一步工作时提前终止执行。它们的行为与异常非常相似,但它们是为非常不同的情况准备的。
让我们看看我们如何使用catch 和throw 来在找到结果后立即结束网络搜索。
def show_rank_for(target, query)
rank = catch(:rank) {
each_google_result_page(query, 6) do |page, page_index|
each_google_result(page) do |result, result_index|
if result.text.include?(target)
throw :rank, (page_index * 10) + result_index
end
end
end
"<not found>"
}
puts "#{target} is ranked #{rank} for search '#{query}'"
end
这一次我们把整个搜索包裹在一个catch{...} 块中。我们告诉catch 块要抓什么符号,在这里是:rank 。当我们找到我们要找的结果时,我们不是设置一个变量,而是抛出符号:rank 。我们也给throw 第二个参数,即搜索结果:rank 。这第二个参数是抛出的 "有效载荷"。
throw "抛出 "执行到catch 块,脱离了所有中间的块和方法调用。因为我们给了throw 和catch 相同的符号(:rank ),catch 块与throw 匹配,抛出的符号被 "捕获"。
我们给出的作为抛出的有效载荷的等级值现在成为catch 块的返回值。我们把结果值分配给一个变量,然后正常进行。
如果从来没有找到搜索字符串,并且throw ,会怎么样?在这种情况下,循环将结束,catch 块的返回值将是该块中最后一条语句的值。我们为这种可能性提供了一个默认值("<not found>" )。
现实世界中的catch和throw
Rack和Sinatra项目提供了一个很好的例子,说明如何使用throw 和catch 来提前终止执行。Sinatra的#last_modified 方法会查看客户端提供的HTTP头信息,如果这些头信息表明客户端已经有了该页面的最新版本,就会立即结束操作并返回 "未修改 "代码。这样就避免了执行整个动作所产生的任何昂贵的处理。
get '/foo' do
last_modified some_timestamp
# ...expensive GET logic...
end
下面是#last_modified 实现的简化版本。注意,它抛出了:halt 符号。Rack抓住了这个符号,并使用提供的响应来立即回复HTTP客户端。不管throw 是在方法调用的几层深处,这都是有效的。
def last_modified(time)
response['Last-Modified'] = time
if request.env['HTTP_IF_MODIFIED_SINCE'] > time
throw :halt, response
end
end
Rack使用catch/throw 的方式说明了一个重要的问题:throw 的调用不一定要和catch 块在同一个方法中。
总结
Ruby是一种试图预测你作为一个程序员的需求的语言。一个常见的需求是,当我们发现没有进一步的工作要做时,可以提前终止执行。不像某些语言,我们不得不滥用异常机制或者使用多个循环中断和方法返回来达到同样的效果,Ruby为我们提供了catch 和throw 机制来快速而干净地提前结束。这样一来,begin/raise/rescue 就可以用来处理错误,而不是其他。