【问题标题】:Why is it bad style to `rescue Exception => e` in Ruby?为什么在 Ruby 中 `rescue Exception => e` 是不好的风格?
【发布时间】:2012-04-20 08:21:30
【问题描述】:

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

不要拯救异常。曾经。否则我会捅你一刀。

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

【问题讨论】:

标签: ruby exception-handling


【解决方案1】:

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


ExceptionRuby's exception hierarchy 的根,因此当您使用rescue Exception 时,您会从一切 中解救出来,包括SyntaxErrorLoadErrorInterrupt 等子类。

拯救Interrupt可以防止用户使用CTRLC退出程序。

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

拯救SyntaxError意味着失败的evals会默默地这样做。

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

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 进行救援。通常,您应该指定比默认的 StandardError 更具体的内容,但从 Exception 中拯救会扩大而不是缩小范围,并且可能会产生灾难性的结果并使寻找错误变得非常困难。 p>


如果您确实想从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

【讨论】:

  • 所以这就像在 java 中捕获 Throwable
  • 这个建议对于干净的 Ruby 环境很有用。但不幸的是,许多 gem 创建了直接源自 Exception 的异常。我们的环境有 30 个:例如OpenID::Server::EncodingError、OAuth::InvalidRequest、HTMLTokenizerSample。这些是您非常希望在标准救援块中捕获的异常。不幸的是,Ruby 中没有任何东西可以阻止甚至阻止 gem 直接从 Exception 继承——即使命名也不直观。
  • @JonathanSwartz 然后从那些特定的子类中拯救出来,而不是例外。更具体几乎总是更好更清晰。
  • @JonathanSwartz - 我会窃听 gem 创建者来更改他们的异常继承自什么。就个人而言,我喜欢我的 gems 的所有异常都来自 MyGemException,所以如果你愿意,你可以拯救它。
  • 你也可以ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]然后rescue *ADAPTER_ERRORS => e
【解决方案2】:

真正的规则是:不要丢弃异常。您引用的作者的客观性值得怀疑,这可以从它以

结尾的事实来证明

否则我会刺伤你

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

#! /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. 如果您正在使用其他人的程序,无论出于何种原因,该程序充满了这些忽略异常块,那么将其放在主线的顶部是一种可能的逃避:

    %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

【讨论】:

  • 对不起,这是错误的。服务器应该从不拯救异常并且除了记录它之外什么都不做。这将使它无法被kill -9杀死。
  • 您在注释 3 中的示例并不等同,ensure 将运行无论是否引发异常,而 rescue 仅在引发异常时运行。
  • 它们不是 /exactly/ 等价的,但我不知道如何以一种不难看的方式简洁地表达等价。
  • 只需在第一个示例中的 begin/rescue 块之后添加另一个 critical_cleanup 调用。我同意不是最优雅的代码,但显然第二个例子是优雅的方式,所以有点不优雅只是例子的一部分。
  • “甚至不要运行它来查看它是否有效。”对编码来说似乎是一个糟糕的建议......相反,我建议你运行它,看到它失败并自己理解如果失败是如何发生的,而不是盲目地相信别人。无论如何都是很好的答案:)
【解决方案3】:

TL;DR

不要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 异常。

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

你发现有什么不对劲,你猛踩紧急休息时间 (^C: Interrupt)

哔哔

警告:捕获中断异常。

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

是的 - 这没有多大帮助。你离铁路很近,所以你把车停在公园里 (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 指出的,确保始终 运行。这很好,因为有时您的程序可能会欺骗您并且不会抛出异常,即使发生问题也是如此。对于关键任务,例如给安全气囊充气,您需要确保它无论如何都会发生。正因为如此,每次停车时检查一下是否抛出异常是一个好主意。尽管在大多数编程环境中给安全气囊充气是一项不常见的任务,但这实际上在大多数清理任务中很常见。

【讨论】:

  • 这个答案是在完全可以理解和正确接受的答案 4 年后出现的,并用一个荒谬的场景重新解释了它,该场景设计得更有趣而不是现实。很抱歉成为一个嗡嗡声,但这不是reddit - 简洁和正确的答案比有趣更重要。此外,关于ensure 替代rescue Exception 的部分具有误导性 - 该示例暗示它们是等效的,但正如ensure 所述,无论是否存在异常都会发生,所以现在你的安全气囊会膨胀,因为你过去了5mph,即使没有出错。
  • @Niall 我更新了我的答案,以使用self.exceeding_safe_stopping_momentum? 更改超过 5mph 检查的误导性。我还添加了一个解释器,说明为什么在这种情况下要使用 ensure。 rescue Exception 并不总是错误的,但 ensure 是进行清理的最佳时机,因为即使您的程序静默失败也会发生这种情况。
【解决方案4】:

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

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

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

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      这篇博文完美地解释了它: Ruby's Exception vs StandardError: What's the difference?

      为什么你不应该拯救异常

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

      这是一个问题,因为使用了一些例外 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
      

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

      幸运的是,有一个简单的方法。

      救援 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
      

      【讨论】:

        猜你喜欢
        • 2013-01-01
        • 2023-01-30
        • 2014-03-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-20
        • 2013-02-10
        • 2011-07-04
        相关资源
        最近更新 更多