为什么在 Ruby 中“拯救 Exception => e”是不好的风格?

Why is it bad style to `rescue Exception => e` in Ruby?

提问人:John 提问时间:4/7/2012 最后编辑:Andrew MarshallJohn 更新时间:2/19/2023 访问量:346228

问:

Ryan Davis 的 Ruby QuickRef 说(没有解释):

不要拯救 Exception。曾。否则我会捅你一刀。

为什么不呢?什么是正确的做法?

Ruby 异常

评论


答:

49赞 Sergio Tulentsev 4/7/2012 #1

因为这会捕获所有异常。您的程序不太可能从其中任何一个中恢复。

您应该只处理您知道如何从中恢复的异常。如果您没有预料到某种类型的异常,请不要处理它,大声崩溃(将详细信息写入日志),然后诊断日志并修复代码。

吞咽异常是不好的,不要这样做。

1464赞 Andrew Marshall 4/7/2012 #2

TL的;DR:改用于常规异常捕获。当重新引发原始异常时(例如,当拯救仅记录异常时),拯救可能是可以的。StandardErrorException


ExceptionRuby 异常层次结构的根源,所以当你从一切事物中拯救出来时,包括 、 和 .rescue ExceptionSyntaxErrorLoadErrorInterrupt

救援可防止用户使用退出程序。InterruptCTRLC

救援会阻止程序正确响应信号。除非 .SignalExceptionkill -9

拯救意味着失败的人会默默地这样做。SyntaxErroreval

所有这些都可以通过运行这个程序来显示,并尝试或它:CTRLCkill

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

救援甚至不是默认设置。行为Exception

begin
  # iceberg!
rescue
  # lifeboats
end

不从中拯救,它从中拯救。您通常应该指定比默认值更具体的内容,但从中救援会扩大范围而不是缩小范围,并且可能会产生灾难性的结果,并使 bug 搜寻变得极其困难。ExceptionStandardErrorStandardErrorException


如果您确实想要从中解救,并且需要一个异常变量,则可以使用以下形式:StandardError

begin
  # iceberg!
rescue => e
  # lifeboats
end

这相当于:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

为数不多的可以挽救的常见情况之一是出于日志记录/报告目的,在这种情况下,应立即重新引发异常:Exception

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end

评论

137赞 ratchet freak 4/7/2012
所以这就像在 Java 中捕捉一样Throwable
54赞 Jonathan Swartz 9/20/2013
这个建议对于一个干净的 Ruby 环境是有好处的。但不幸的是,许多 gem 创建了直接源自 Exception 的异常。我们的环境有 30 个这样的:例如 OpenID::Server::EncodingError、OAuth::InvalidRequest、HTMLTokenizerSample。这些是您非常希望在标准救援块中捕获的例外情况。不幸的是,Ruby 中没有任何东西阻止甚至阻止 gem 直接从 Exception 继承——甚至命名也不直观。
20赞 Andrew Marshall 9/20/2013
@JonathanSwartz 然后从这些特定的子类中拯救,而不是 Exception。更具体几乎总是更好、更清晰。
24赞 Nathan Long 2/1/2014
@JonathanSwartz - 我会打扰 gem 创建者更改他们的异常继承的内容。就我个人而言,我喜欢我的宝石让所有异常都来自 MyGemException,所以如果你愿意,你可以挽救它。
16赞 j_mcnally 2/27/2014
你也可以,然后ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]rescue *ADAPTER_ERRORS => e
13赞 Russell Borogove 4/7/2012 #3

这是规则的一个特定情况,你不应该发现任何你不知道如何处理的异常。如果您不知道如何处理它,最好让系统的其他部分捕获并处理它。

86赞 Michael Slade 4/7/2012 #4

真正的规则是:不要抛弃例外。你引用的作者的客观性是值得怀疑的,它以

否则我会捅你一刀

当然,请注意,信号(默认情况下)会抛出异常,并且通常长时间运行的进程会通过信号终止,因此捕获异常而不在信号异常时终止将使您的程序很难停止。所以不要这样做:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

不,真的,不要这样做。甚至不要运行它来查看它是否有效。

但是,假设您有一个线程服务器,并且您希望所有异常都不:

  1. 被忽略(默认值)
  2. 停止服务器(如果您说 )。thread.abort_on_exception = true

那么这在您的连接处理线程中是完全可以接受的:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

以上是 Ruby 默认异常处理程序的变体,其优点是它不会杀死你的程序。Rails 在其请求处理程序中做到了这一点。

在主线程中引发信号异常。后台线程不会得到它们,所以试图在那里捕捉它们是没有意义的。

这在生产环境中特别有用,因为在生产环境中,您不希望程序在出现问题时就停止。然后,您可以在日志中获取堆栈转储,并将其添加到代码中,以更优雅的方式处理调用链中更下游的特定异常。

还要注意的是,还有另一个 Ruby 成语,其效果大致相同:

a = do_something rescue "something else"

在这一行中,如果引发异常,它将被 Ruby 捕获、丢弃并被分配。do_somethinga"something else"

一般来说,不要这样做,除非在特殊情况下,你知道你不需要担心。举个例子:

debugger rescue nil

该函数是在代码中设置断点的一种相当不错的方法,但如果在调试器和 Rails 之外运行,则会引发异常。现在,从理论上讲,你不应该把调试代码留在你的程序中(pff!没有人这样做!),但你可能出于某种原因想把它保留一段时间,但不要继续运行你的调试器。debugger

注意:

  1. 如果您运行了其他人的程序来捕获信号异常并忽略它们,(例如上面的代码),那么:

    • 在 Linux 中,在 shell 中键入 ,或 ,查找有问题的程序的 PID,然后运行 。pgrep rubyps | grep rubykill -9 <PID>
    • 在 Windows 中,使用任务管理器 (--),转到“进程”选项卡,找到您的进程,右键单击它并选择“结束进程”。CTRLSHIFTESC
  2. 如果你正在使用别人的程序,无论出于何种原因,都充斥着这些忽略异常块,那么把它放在主线的顶部是一种可能的解决方法:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }
    

    这会导致程序通过立即终止、绕过异常处理程序来响应正常的终止信号,而无需清理。因此,它可能会导致数据丢失或类似情况。小心!

  3. 如果需要执行此操作:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end
    

    您实际上可以这样做:

    begin
      do_something
    ensure
      critical_cleanup
    end
    

    在第二种情况下,无论是否引发异常,每次都会调用。critical cleanup

评论

8赞 Andrew Marshall 12/8/2012
注释 3 中的示例并不等价,无论是否引发异常都会运行,而 只会在引发异常时运行。ensurerescue
1赞 Michael Slade 1/31/2013
它们不是/完全/等价的,但我不知道如何以一种不丑陋的方式简洁地表达等价。
3赞 gtd 3/6/2013
在第一个示例中,只需在 begin/rescue 块之后添加另一个 critical_cleanup 调用即可。我同意不是最优雅的代码,但显然第二个例子是优雅的执行方式,所以一点点不优雅只是例子的一部分。
3赞 huelbois 11/9/2016
“甚至不要运行它来查看它是否有效”,这似乎是编码的坏建议......相反,我建议你运行它,看到它失败,并自己理解如果失败,而不是盲目地相信别人。无论如何,很好的答案:)
1赞 beckah 9/8/2017
"The objectivity of the author of your quote is questionable".这家伙写了 minitest 和大量其他广泛使用的宝石。blog.zenspider.com/projects
80赞 Ben Aubin 2/1/2016 #5

TL的;博士

不要(也不要重新提出异常)——否则你可能会从桥上开车下来。rescue Exception => e


假设你在一辆车里(运行 Ruby)。您最近安装了带有无线升级系统的新方向盘(使用 ),但您不知道其中一位程序员搞砸了语法。eval

你在一座桥上,意识到你要朝栏杆走一点,所以你左转。

def turn_left
  self.turn left:
end

哎呀!这可能不好™,幸运的是,Ruby 提出了一个.SyntaxError

汽车应该立即停下来——对吧?

不。

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

哔哔

警告:捕获了 SyntaxError 异常。

信息:记录的错误 - 正在继续。

你注意到有些不对劲,你猛地按下了紧急休息键(:^CInterrupt)

哔哔

警告:捕获中断异常。

信息:记录的错误 - 正在继续。

是的 - 这没有多大帮助。你离铁轨很近,所以你把车停在停车场(ing:)。killSignalException

哔哔

警告:捕获 SignalException 异常。

信息:记录的错误 - 正在继续。

在最后一秒,你拔出钥匙(),汽车停了下来,你向前猛地撞上了方向盘(安全气囊不能充气,因为你没有优雅地停止程序 - 你终止了它),你汽车后部的电脑砰地一声撞上了它前面的座位。一罐半满的可乐洒在纸上。后面的杂货被压碎了,大部分都覆盖着蛋黄和牛奶。这辆车需要认真维修和清洁。(数据丢失)kill -9

希望你有保险(备份)。哦,是的 - 因为安全气囊没有充气,你可能会受伤(被解雇等)。


但是等等!您可能想要使用的原因还有很多rescue Exception => e

假设您是那辆车,如果汽车超过其安全停车动力,您要确保安全气囊充气。

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

以下是该规则的例外情况:只有在重新引发异常时才能捕获。因此,更好的规则是永远不要吞咽,并且总是重新提出错误。ExceptionException

但是,在像 Ruby 这样的语言中,添加救援很容易被遗忘,而且在重新提出问题之前立即发布救援声明感觉有点不干涩。而且你不想忘记这句话。如果你这样做了,祝你好运,试图找到那个错误。raise

值得庆幸的是,Ruby 很棒,你可以只使用关键字,这可以确保代码运行。无论如何,该关键字都会运行代码 - 如果抛出异常,如果没有,唯一的例外是世界结束(或其他不太可能的事件)。ensureensure

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

繁荣!无论如何,该代码都应该运行。您应该使用的唯一原因是,如果需要访问异常,或者只想在异常上运行代码。并记住重新提出错误。每次。rescue Exception => e

注意:如@Niall指出,请确保始终运行。这很好,因为有时你的程序可以对你撒谎,即使出现问题也不会引发异常。对于关键任务,例如给安全气囊充气,您需要确保无论如何都会发生。因此,每次汽车停车时检查是否抛出异常是一个好主意。尽管在大多数编程环境中给安全气囊充气是一项不常见的任务,但这实际上在大多数清理任务中很常见。

评论

6赞 Niall 7/31/2018
这个答案是在完全可以理解和正确接受的答案 4 年后出现的,并用一个荒谬的场景重新解释了它,该场景设计得比现实更有趣。很抱歉成为一个嗡嗡声,但这不是reddit - 答案简洁正确比有趣更重要。此外,作为替代方案的部分具有误导性 - 该示例暗示它们是等效的,但如前所述,无论是否有例外都会发生,所以现在您的安全气囊会膨胀,因为即使您没有任何问题,您的时速也超过了 5 英里/小时。ensurerescue Exceptionensure
0赞 Ben Aubin 8/4/2018
@Niall 我更新了我的答案,以更改超过 5 英里/小时的误导性检查。我还添加了一个解释器,说明为什么在这种情况下要使用 sure。 并不总是错的,但通常是在发生清理时进行清理的更好时机,即使您的程序以静默方式失败。self.exceeding_safe_stopping_momentum?rescue Exceptionensure
13赞 calebkm 11/7/2019 #6

这篇博文完美地解释了这一点:Ruby 的异常与标准错误:有什么区别?

为什么你不应该拯救异常

拯救异常的问题 是它实际上拯救了继承自 例外。这是....所有的人!

这是一个问题,因为使用了一些例外 内部由 Ruby 提供。它们与您的应用程序没有任何关系,并且 吞下它们会导致坏事发生。

以下是一些重要的:

  • SignalException::Interrupt - 如果挽救此漏洞,则无法退出 应用程序通过按 Control-C 进行。

  • ScriptError::SyntaxError - 吞咽语法错误意味着事情 like puts(“忘记了什么)会悄无声息地失败。

  • NoMemoryError - 想知道当程序保持时会发生什么 在它用完所有 RAM 后运行?我也没有。

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

我猜你真的不想吞下这些东西 系统级异常。你只想抓住你所有的 应用程序级错误。异常导致了您的代码。

幸运的是,有一种简单的方法可以做到这一点。

改为救援 StandardError

应关注的所有异常都继承自 StandardError。这些是我们的老朋友:

NoMethodError - 尝试调用不存在的方法时引发

TypeError - 由 1 + “” 等原因引起

RuntimeError - 谁能忘记旧的 RuntimeError?

若要挽救此类错误,需要挽救 StandardError。你可以通过写这样的东西来做到这一点:

begin
  do_something()
rescue StandardError => e
  # Only your app's exceptions are swallowed. Things like SyntaxErrror are left alone. 
end

但 Ruby 让它更容易使用。

当你根本没有指定异常类时,ruby 会假设你的意思是 StandardError。所以下面的代码与上面的代码是一样的:

begin
  do_something()
rescue => e
  # This is the same as rescuing StandardError
end