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

Joh*_*ohn 876 ruby exception-handling

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

不要救援Exception.EVER.或者我会刺伤你

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

And*_*all 1348

TL; DR:StandardError代替一般异常捕获.当重新引发原始异常时(例如,当救援仅记录异常时),救援Exception可能没问题.


Exception是根Ruby的异常层次结构,所以当你rescue Exception从拯救一切,包括子类,如SyntaxError,LoadErrorInterrupt.

拯救会Interrupt阻止用户CTRLC退出程序.

抢救SignalException可防止程序正确响应信号.除了之外,它将是不可杀戮的kill -9.

拯救SyntaxError意味着eval失败的s会默默地这样做.

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

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end
Run Code Online (Sandbox Code Playgroud)

拯救Exception甚至不是默认值.干

begin
  # iceberg!
rescue
  # lifeboats
end
Run Code Online (Sandbox Code Playgroud)

没有救出Exception,它从中拯救出来StandardError.您通常应该指定比默认值更具体的内容StandardError,但是从Exception 扩大范围而不是缩小范围开始,可能会产生灾难性的结果并使得捕获bug非常困难.


如果您确实需要进行救援,StandardError并且需要具有异常的变量,则可以使用以下形式:

begin
  # iceberg!
rescue => e
  # lifeboats
end
Run Code Online (Sandbox Code Playgroud)

这相当于:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end
Run Code Online (Sandbox Code Playgroud)

为了挽救Exception日志,为数不多的常见案例之一是用于记录/报告目的,在这种情况下,您应立即重新提出异常:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Run Code Online (Sandbox Code Playgroud)

  • 所以就像在java中捕捉`Throwable`一样 (127认同)
  • 这个建议适用于干净的Ruby环境.但不幸的是,许多宝石创造了直接来自异常的异常.我们的环境有30个:例如OpenID :: Server :: EncodingError,OAuth :: InvalidRequest,HTMLTokenizerSample.这些是您非常希望在标准救援块中捕获的例外情况.不幸的是,Ruby中的任何内容都无法阻止甚至阻止Gem直接从Exception继承 - 即使命名也不直观. (53认同)
  • @JonathanSwartz - 我会让gem创建者改变他们的异常继承的东西.就个人而言,我喜欢我的宝石,让所有异常都来自MyGemException,所以如果你愿意,你可以拯救它. (22认同)
  • @JonathanSwartz然后从那些特定的子类中解救,而不是Exception.更具体的几乎总是更好,更清晰. (20认同)
  • @Excalibur如果你重新提出异常,那就没事了,因为你没有吞下它,只是试着知道它发生了然后让它冒出来.通常用于记录. (11认同)
  • 你也可以`ADAPTER_ERRORS = [:: ActiveRecord :: StatementInvalid,PGError,Mysql :: Error,Mysql2 :: Error,:: ActiveRecord :: JDBCError,SQLite3 :: Exception]`然后`rescue*ADAPTER_ERRORS => e` (11认同)
  • 我刚刚找到另一个例子,为什么`救援Exception`很糟糕:它在我们的规范中救出了失败!在我们的代码库中甚至有一个`should_not_receive`,作者显然信任规范和思想,它已经实现了:但实际上这个方法被调用,只是失败被救出:( (8认同)
  • 安德鲁 - 有很多次你想要捕捉所有标准的例外.您自己提到了一个 - 如果您想在消息中添加一些上下文,那么请重新抛出或记录或空中制动它. (3认同)
  • 所以,现在我只需要确保我们的宝石不会引发Exception或一些超级自定义异常,它是Exception的直接子类! (2认同)

Mic*_*ade 82

真正的规则是:不要扔掉异常.你引用的作者的客观性是值得怀疑的,正如它的结尾所证明的那样

或者我会刺伤你

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

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end
Run Code Online (Sandbox Code Playgroud)

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

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

  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
Run Code Online (Sandbox Code Playgroud)

以上是Ruby的默认异常处理程序的变体,其优点是它也不会杀死你的程序.Rails在其请求处理程序中执行此操作.

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

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

另请注意,还有另一个Ruby习语具有相同的效果:

a = do_something rescue "something else"
Run Code Online (Sandbox Code Playgroud)

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

一般情况下,不这样做,除非在你特殊的情况下知道你并不需要担心.一个例子:

debugger rescue nil
Run Code Online (Sandbox Code Playgroud)

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

注意:

  1. 如果你运行别人的程序捕获信号异常并忽略它们(比如上面的代码)那么:

    • 在Linux中,在shell中,键入pgrep rubyps | grep ruby查找违规程序的PID,然后运行kill -9 <PID>.
    • 在Windows中,使用任务管理器(CTRL- SHIFT- ESC),转到"进程"选项卡,找到您的进程,右键单击它并选择"结束进程".
  2. 如果您正在使用其他人的程序,无论出于何种原因,使用这些忽略异常块,那么将其放在主线的顶部就是一个可能的问题:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }
    
    Run Code Online (Sandbox Code Playgroud)

    这导致程序通过立即终止,绕过异常处理程序而不进行清理来响应正常终止信号 .因此它可能导致数据丢失或类似.小心!

  3. 如果你需要这样做:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end
    
    Run Code Online (Sandbox Code Playgroud)

    你实际上可以这样做:

    begin
      do_something
    ensure
      critical_cleanup
    end
    
    Run Code Online (Sandbox Code Playgroud)

    在第二种情况下,critical cleanup每次都会调用,无论是否抛出异常.

  • 对不起,这是错的.服务器应该_never_ rescue Exception并且不做任何事情,只需记录它.除了`kill -9`之外,这将使它无法杀死. (21认同)
  • 注释3中的示例不是等价的,无论是否引发异常,都会运行`ensure`,而`rescue`只会在引发异常时运行. (8认同)
  • 只需在第一个示例中的begin/rescue块之后添加另一个critical_cleanup调用.我不同意最优雅的代码,但显然第二个例子是优雅的方式,所以有点不雅只是例子的一部分. (3认同)
  • "甚至不要跑它,看它是否有效." 对于编码似乎是一个糟糕的建议...相反,我会建议你运行它,看它失败并自己理解如果失败,而不是盲目地相信别人.反正很棒的答案:) (3认同)

Ben*_*bin 63

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

你在桥上,意识到你正朝着栏杆前进,所以你向左转.

def turn_left
  self.turn left:
end
Run Code Online (Sandbox Code Playgroud)

哎呀!这可能是不好的,幸运的是,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
Run Code Online (Sandbox Code Playgroud)

哔哔

警告:捕获SyntaxError异常.

信息:记录错误 - 继续过程.

您会发现什么是错的,你猛踩紧急中断(^C:Interrupt)

哔哔

警告:捕获中断异常.

信息:记录错误 - 继续过程.

是的 - 这没多大帮助.你离铁轨很近,所以你把车停在公园里(killing :) SignalException.

哔哔

警告:Caught SignalException异常.

信息:记录错误 - 继续过程.

在最后一秒,你拉出钥匙(kill -9),汽车停下来,你向前撞到方向盘(安全气囊不能充气,因为你没有优雅地停止程序 - 你终止了它)和计算机在你的车后面猛烈撞入它前面的座位.半满的可乐可以溢出纸张.背面的杂货被粉碎,大部分都是蛋黄和牛奶.汽车需要严格的维修和清洁.(数据丢失)

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


可是等等!有更多你可能想要使用的原因rescue Exception => e!

假设你是那辆车,如果汽车超过其安全停止动力,你想确保安全气囊膨胀.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end
Run Code Online (Sandbox Code Playgroud)

以下是规则的例外情况:Exception 只有在重新引发异常时才能捕获.因此,更好的规则是永不吞咽Exception,并始终重新提出错误.

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

值得庆幸的是,Ruby非常棒,你可以使用ensure关键字来确保代码运行.该ensure关键字将不管运行代码-如果一个异常被抛出,如果不是,唯一的例外是,如果世界结束(或其他不可能事件).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end
Run Code Online (Sandbox Code Playgroud)

繁荣!并且该代码应该运行.您应该使用的唯一原因rescue Exception => e是您需要访问异常,或者您只希望代码在异常上运行.并记得重新提出错误.每次.

注意:正如@Niall指出的那样,确保始终运行.这很好,因为有时你的程序会骗你,而不会抛出异常,即使出现问题.对于像气囊充气这样的关键任务,无论如何都要确保它发生.因此,每次停车检查是否抛出异常都是一个好主意.尽管在大多数编程环境中充气安全气囊是一项不常见的任务,但实际上这对于大多数清理任务来说都很常见.


TL; DR

不要rescue Exception => e(而不是重新提出异常) - 或者你可能会开出一座桥.

  • 哈哈哈哈!这是一个很好的答案.我很震惊,没有人评论过.你给出一个明确的场景,使整个事情真的可以理解.干杯! :-) (11认同)
  • +此答案。希望我能投票一次以上! (3认同)
  • 这个答案是在完全可以理解和正确接受的答案之后的四年,并以一种荒谬的场景重新解释了这个场景,该场景设计得比现实更有趣。抱歉,成为嗡嗡声,但这不是Reddit-答案简洁,正确而不是有趣更重要。另外,关于“确保”替代“救援异常”的部分具有误导性-该示例暗示它们是等效的,但是如上所述,“确保”无论是否存在异常都会发生,因此现在您的安全气囊会膨胀,因为您去过超过5英里/小时,即使没有任何问题。 (2认同)

Ser*_*sev 46

因为这会捕获所有异常.您的程序不可能从任何程序中恢复.

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

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


小智 15

这篇博文完美地解释了这一点: Ruby's Exception vs StandardError: What’s the Difference?

为什么你不应该拯救异常

拯救Exception的问题在于它实际上拯救了从Exception继承的每个异常。这是……全部!

这是一个问题,因为 Ruby 内部使用了一些异常。它们与您的应用程序没有任何关系,吞下它们会导致不好的事情发生。

以下是一些重要的:

  • SignalException::Interrupt - 如果您挽救此问题,则无法通过按 control-c 退出应用程序。

  • ScriptError::SyntaxError - 吞咽语法错误意味着像 put("Forgot Something) 这样的事情会默默地失败。

  • NoMemoryError - 想知道当你的程序用完所有 RAM 后继续运行时会发生什么?我也不。

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

我猜你并不想接受任何这些系统级异常。您只想捕获所有应用程序级别的错误。异常导致了您的代码。

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

改为拯救 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
Run Code Online (Sandbox Code Playgroud)

但 Ruby 让它更容易使用。

当您根本不指定异常类时,Ruby 会假定您指的是 StandardError。所以下面的代码与上面的代码是相同的:

begin
  do_something()
rescue => e
  # This is the same as rescuing StandardError
end
Run Code Online (Sandbox Code Playgroud)


Rus*_*ove 9

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