为什么在Ruby中使用`rescue Exception => e`样式不好?


893

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

不要挽救异常。永远 不然我会刺你

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


35
那你可能可以自己写?:)
Sergio Tulentsev 2012年

65
我对这里的暴力呼吁感到非常不自在。这只是编程。
Darth Egregious 2015年

1
通过一个很好的Ruby Exception Hierarchy查看Ruby Exception 中的这篇文章
Atul Khanduri 2015年

2
因为瑞安·戴维斯(Ryan Davis)会刺你。孩子们 永远不要挽救异常。
穆根'18

7
@DarthEgregious我真的不能告诉你是否在开玩笑。但我认为这很有趣。(这显然不是严重的威胁)。现在,每当我想到捕获Exception时,我都会考虑是否值得在Internet上随意刺杀某个人。
史蒂夫·塞尔

Answers:


1374

TL; DRStandardError改为用于常规异常捕获。重新引发原始异常时(例如,在救援时仅记录异常),救援Exception可能就可以了。


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

救援Interrupt可以防止用户CTRLC用来退出程序。

抢救会SignalException阻止程序正确响应信号。除以外,它将是不可杀死的kill -9

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

所有这些都可以通过运行这个程序,并试图显示CTRLC或者kill它:

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

不从中营救,从中Exception营救StandardError。通常,您应该指定比default更为具体的内容StandardError,但是从中进行救援可以Exception 扩大范围而不是缩小范围,并且可能导致灾难性的结果,并且使错误查找变得极为困难。


如果您确实想从中解脱,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

129
就像抓ThrowableJava一样
棘轮怪胎2012年

53
该建议对于干净的Ruby环境很有用。但是不幸的是,许多宝石创造了直接源自Exception的异常。我们的环境中有30种:例如OpenID :: Server :: EncodingError,OAuth :: InvalidRequest,HTMLTokenizerSample。这些是您非常想在标准救援块中捕获的例外。不幸的是,Ruby中没有任何东西可以阻止甚至阻止宝石直接从Exception继承,甚至命名也不是很直观的。
乔纳森·斯沃兹

20
@JonathanSwartz然后从那些特定的子类中抢救,而不是从Exception中抢救出来。更具针对性几乎总是更好,更清晰。
Andrew Marshall

22
@JonathanSwartz-我会调试gem创造者以更改其异常继承的内容。就我个人而言,我希望我的gem的所有异常都源于MyGemException,因此,如果需要,您可以进行挽救。
内森·朗

12
您还可以ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]然后rescue *ADAPTER_ERRORS => e
j_mcnally 2014年

83

真正的规则是:不要扔掉异常。您的报价作者的客观性值得怀疑,事实证明是这样的事实:

不然我会刺你

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

#! /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"

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

一般情况下,不这样做,除非在你特殊情况下,知道你没有必要担心。一个例子:

debugger rescue nil

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

注意:

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

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

    %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无论是否引发异常,都会被调用。


21
抱歉,这是错误的。服务器切勿营救Exception,而只能对其进行记录。除之外,这将使其无法杀死kill -9
约翰

8
您在注释3中的示例不是等价的,ensure无论是否引发异常,an 都会运行,而rescue仅在引发异常时才会运行will。
安德鲁·马歇尔

1
它们不是/ exactly /等价的,但是我无法弄清楚如何以一种不丑陋的方式简洁地表达对等物。
Michael Slade

3
在第一个示例中,只需在begin / rescue块之后添加另一个critical_cleanup调用即可。我同意的不是最优雅的代码,但是显然第二个示例是优雅的实现方式,因此,一点点优雅只是该示例的一部分。
gtd

3
“甚至不要运行它来查看它是否有效。” 似乎是不好的编码建议。相反,我建议您运行它,看看它是否失败,并亲自了解如果失败了,而不是盲目地相信别人。无论如何,很好的答案:)
huelbois

69

TL; DR

请勿rescue Exception => e(也不要重新提出例外)-否则您可能会驶离桥梁。


假设您正在开车(正在运行Ruby)。您最近在空中升级系统上安装了一个新的方向盘(使用eval),但您不知道其中一位程序员对语法感到困惑。

您在桥上,意识到自己要驶向栏杆,所以您向左转。

def turn_left
  self.turn left:
end

哎呀!幸运的是,这可能不是Good ™,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

哔哔

警告:捕获到中断异常。

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

是的-没什么帮助。您非常靠近铁路,因此您将汽车停放在了公园(killing:)SignalException

哔哔

警告:捕获到SignalException异常。

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

在最后一秒钟,您拉出(kill -9)键,汽车停下来,向前猛撞方向盘(安全气囊无法充气,因为您没有适当地停止程序,因此终止了程序),然后计算机后面的汽车猛撞到它前面的座位上。半满的可乐罐溢出了报纸。后面的杂物被压碎,大部分被蛋黄和牛奶覆盖。该车需要认真维修和清洁。(数据丢失)

希望您有保险(备用)。哦,是的-由于安全气囊没有膨胀,您可能受伤了(被解雇了,等等)。


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

假设您是那辆汽车,并且如果汽车超过其安全的停止动量,则要确保安全气囊膨胀。

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

这是该规则的例外:Exception 只有重新引发例外时,您才能捕获。因此,更好的规则是永不吞咽Exception,并始终重新提出该错误。

但是,使用Ruby之类的语言添加救援很容易被遗忘,并且在重新提出问题之前立即发布救援声明会感到有些不愉快。而你想要忘记的raise声明。如果这样做,祝您找到这个错误,祝您好运。

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

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

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

注意:正如@Niall指出的那样,请确保始终运行。这是件好事,因为有时即使您遇到问题,您的程序也可以骗您,并且不会抛出异常。对于关键任务,例如给安全气囊充气,您需要确保无论发生什么事情都可以发生。因此,每次停车时检查是否引发异常都是一个好主意。尽管在大多数编程情况下,安全气囊的充气并不常见,但实际上在大多数清理任务中还是很常见的。


12
哈哈哈哈!这是一个很好的答案。我很震惊,没有人发表评论。您给出了一个清晰的场景,使整个事情真正变得可以理解。干杯! :-)
詹姆斯·米兰尼

@JamesMilani谢谢!
本·奥宾

3
+💯这个答案。希望我能投票一次以上!😂–
EngineerDave

1
喜欢你的答案!
Atul Vaibhav

3
这个答案是在完全可以理解和正确接受的答案之后的四年,并以一种荒谬的场景重新解释了这个场景,该场景设计得比现实更有趣。抱歉,成为嗡嗡声,但这不是Reddit-答案简洁,正确而不是有趣更重要。另外,关于ensure的替代部分rescue Exception具有误导性-该示例暗示它们是等效的,但是ensure如上所述,无论是否存在异常都将发生,因此现在您的安全气囊将膨胀,因为即使没有发生任何问题,行驶速度超过5英里/小时。
Niall

47

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

您应该只处理知道如何恢复的异常。如果您不希望发生某种异常,请不要处理它,大声崩溃(将详细信息写入日志),然后诊断日志并修复代码。

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



0

我刚刚在honeybadger.io上阅读了一篇很棒的博客文章:

Ruby的Exception与StandardError:有什么区别?

为什么你不应该拯救异常

抢救Exception的问题在于,它实际上可以挽救从Exception继承的每个异常。那是....所有人!

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

以下是一些大问题:

  • SignalException :: Interrupt-如果您营救了这个,则无法通过按Control-c退出应用程序。

  • ScriptError :: SyntaxError-吞咽语法错误意味着puts(“ 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

我猜您真的不想吞下任何这些系统级异常。您只想捕获所有应用程序级错误。异常导致您的代码。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.