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

瑞安·戴维斯的rubyquickref说(没有解释):

不要救例外。永远。否则我会刺伤你。

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

329688 次浏览

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

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

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

太长别读:使用StandardError代替常规异常捕获。当重新引发原始异常时(例如,仅当救援记录异常时),救援Exception可能是可以的。


ExceptionRuby的异常层次结构的根,所以当你rescue Exception时,你从一切中拯救出来,包括子类,如SyntaxErrorLoadErrorInterrupt

救援Interrupt阻止用户使用CTRLC退出程序。

拯救SignalException会阻止程序正确响应信号。除非kill -9,否则它将无法杀死。

拯救SyntaxError意味着失败的eval将默默地这样做。

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

loop dobeginsleep 1eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"rescue Exceptionputs "I refuse to fail or be stopped!"endend

Exception中拯救甚至不是默认的。做

begin# iceberg!rescue# lifeboatsend

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


如果你确实想从StandardError中拯救,并且你需要一个带有异常的变量,你可以使用这种形式:

begin# iceberg!rescue => e# lifeboatsend

这相当于:

begin# iceberg!rescue StandardError => e# lifeboatsend

Exception中拯救是合理的少数常见情况之一是用于日志记录/报告目的,在这种情况下,您应该立即重新引发异常:

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

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

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

不然我就捅死你

当然,要注意信号(默认情况下)会抛出异常,通常长时间运行的进程会通过信号终止,所以捕捉异常而不终止信号异常会让你的程序很难停止。所以不要这样做:

#! /usr/bin/ruby
while true dobeginline = STDIN.gets# heavy processingrescue Exception => eputs "caught exception #{e}! ohnoes!"endend

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

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

  1. 被忽略(默认值)
  2. 停止服务器(如果您说thread.abort_on_exception = true会发生这种情况)。

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

begin# do stuffrescue Exception => emyLogger.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. 如果你正在使用别人的程序,无论出于什么原因,都充斥着这些忽略异常块,那么将其放在主线的顶部是一种可能的逃避:

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

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

  3. 如果你需要这样做:

    begindo_somethingrescue Exception => ecritical_cleanupraiseend

    你实际上可以这样做:

    begindo_somethingensurecritical_cleanupend

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

太长别读

不要rescue Exception => e(并且不要重新引发异常)-否则您将可能从桥上开车。


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

你在一座桥上,意识到你正朝着栏杆走去,所以你向左转。

def turn_leftself.turn left:end

哎呀!这可能是情况不妙™,幸运的是,Ruby提出了SyntaxError

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

没有。

begin#...eval self.steering_wheel#...rescue Exception => eself.beepself.log "Caught #{e}.", :warnself.log "Logged Error - Continuing Process.", :infoend

哔哔

警告:捕获了Syn的错误异常。

信息:日志错误-持续过程。

你注意到有什么不对劲,你猛击紧急休息(^CInterrupt

哔哔

警告:捕获中断异常。

信息:日志错误-持续过程。

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

哔哔

警告:捕获信号异常异常。

信息:日志错误-持续过程。

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

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


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

假设你是那辆车,你想确保安全气囊膨胀,如果汽车超过了它的安全停止动量。

 begin# do driving stuffrescue Exception => eself.airbags.inflate if self.exceeding_safe_stopping_momentum?raiseend

这是规则的例外:你可以捕获Exception除非你重新提出例外。因此,更好的规则是永远不要吞下Exception,并始终重新引发错误。

但是在像Ruby这样的语言中添加救援很容易忘记,并且在重新提出问题之前放置救援声明感觉有点不DRY。并且您想忘记raise语句。如果你这样做了,祝你好运试图找到那个错误。

幸运的是,Ruby很棒,你可以使用ensure关键字,它确保代码运行。ensure关键字无论如何都会运行代码——如果抛出异常,如果没有,唯一的例外是世界末日(或其他不太可能的事件)。

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

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

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

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

为什么你不应该拯救例外

解救异常的问题它实际上拯救了继承自例外……所有的人!

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

这里有几个大的:

  • SignalException::中断-如果你拯救这个,你不能退出你的点击控制-c。

  • ScriptError::语法错误-吞咽语法错误意味着事情就像放(“忘记了什么”)会默默失败。

  • NoMemoryError-想知道当你的程序保持时会发生什么运行它会耗尽所有的RAM?我也是。

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

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

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

救援标准错误

您应该关心的所有异常都继承自Standard ardError。这些是我们的老朋友:

当您尝试调用不存在的方法时引发

TypeError-由1+""之类的东西引起

RuntimeError-谁能忘记好的老RuntimeError?

要拯救这样的错误,你需要拯救Standard ardError。你可以通过编写这样的内容来做到这一点:

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

但Ruby使其更易于使用。

当你根本不指定异常类时,ruby假设你是指Standard ardError。所以下面的代码与上面的代码相同:

begindo_something()rescue => e# This is the same as rescuing StandardErrorend