原文: A Beginner’s Guide to Exceptions in Ruby

什么是异常

异常是ruby处理意外错误的方式。 如果输入了错误的代码,就会导致程序崩溃时出现类似SyntaxError或NoMethodError类型的错误,那就是异常信息。 当在Ruby中引发异常时,程序将退出并出现错误消息。

在下面尝试除以零的代码中,Ruby会引发一个称为ZeroDivisionError的异常,程序退出并出现错误消息。

1 / 0
# 程序退出并提示: ZeroDivisionError: divided by 0

崩溃往往会给程序带来糟糕的使用体验,所以我们通常想要阻止它崩溃并智能地对错误进行处理。

这称之为『解救』(rescuing)、『处理』(handing) 或『捕获』(catching)异常,通常叫捕获,在Ruby中会这样做:

begin
  # 出错的代码
  1/0
rescue
  # 出错时,这里的代码会执行
  puts "找到一个错误!"
  do_something_intelligent()
end

# 这段代码没有崩溃
# 输出: "找到一个错误!"

上面的代码虽然发生了异常,但不会导致程序崩溃,因为它被捕获了,Ruby运行捕获块中的代码并输出一条消息。 虽然看起来不错,但还是有一个很大的问题,代码虽然告诉我们出了问题,但我们还不知道究竟发生了什么问题,因为所有相关的错误信息都包含在异常对象中。

异常对象

异常对象是普通的Ruby对象,保存了刚刚被捕获的异常的所有错误信息。 要获取异常对象,就需要用另一个稍微不同的『捕获』语法。

# 捕获所有的错误,请错误信息放到异常对象`e`中
rescue => e

# 只捕获ZeroDivisionError错误并将错误信息放到`e`中
rescue ZeroDivisionError => e

在上面第二个例子中,ZeroDivisionError是错误对象e的类,这里所说的所有『类型』的异常只是『类名』。

异常对象还保存着有用的调试信息,下面用代码看一下ZeroDivisionError的异常对象。

begin
  # 出错的代码
  1/0
rescue ZeroDivisionError => e
  puts "异常类(Exception Class): #{ e.class.name }"
  puts "异常消息(Exception Message): #{ e.message }"
  puts "异常追踪(Exception Backtrace): #{ e.backtrace }"
end

# Outputs:
# 异常类(Exception Class): ZeroDivisionError
# 异常消息(Exception Messge): divided by 0
# 异常追踪(Exception Backrace): ["exception.rb:3:in `/'", "exception.rb:3:in `<main>'"]

像大多数Ruby异常一样,它包含了异常的类名、异常消息以及异常的追踪信息。

抛出异常

目前为止我们只谈到捕获异常。除此之外还可以通过raise方法触发(抛出)自己的异常。 抛出自己的异常时,可以自己选择和定义错误类型以及错误消息。

begin
  # 抛出 ArgumentError 异常和错误消息 "you messed up!"
  raise ArgumentError.new("You messed up!")
rescue ArgumentError => e
  puts e.message
end

# 输出: You messed up!

上面代码抛出了一个ArgumentError错误对象和一个自定义的错误消息”you messed up!”,并将其传递给了raise方法。

还有其它方式调用raise:

# 运行时错误(RuntimeError)
raise RuntimeError.new("You messed up!")

# 和上面代码一样的效果
raise RuntimeError, "You messed up!"

# 同上,只需要设用raise方法,并设置错误消息
# RuntimeErrors this way
raise "You messed up!"

自定义异常

虽然Ruby有很多内置的异常,但也不能涵盖所有可能发生的错误。

比如正在写一个用户系统,想要在用户试图访问限制内容时抛出异常,Ruby的标准异常都不太合适,最好的方法是创建一个新的异常类型。

要创建自定义异常,只需创建一个新类并继承自StandardError类。

class PermissionDeniedError < StandardError
end
raise PermissionDeniedError.new()

这是一个普通的ruby类,我们可以像所有其他类一样添加方法和属性,比如添加一个名为action的属性:

class PermissionDeniedError < StandardError

  attr_reader :action

  def initialize(message, action)
    # 调用父类的构造函数来设置消息
    super(message)

    # 将action存入实例变量中
    @action = action
  end

end

# 然后当用户试图删除他们不应该删除的东西时,可以这样做
raise PermissionDeniedError.new("Permission Denied", :delete)

异常对象层级

Exception
  NoMemoryError
  ScriptError
    LoadError
    NotImplementedError
    SyntaxError
  SignalException
    Interrupt
  StandardError
    ArgumentError
    IOError
      EOFError
    IndexError
    LocalJumpError
    NameError
      NoMethodError
    RangeError
      FloatDomainError
    RegexpError
    RuntimeError
    SecurityError
    SystemCallError
    SystemStackError
    ThreadError
    TypeError
    ZeroDivisionError
  SystemExit

从上面的层级关系看,StandardError和其它异常一样,最终都是基于Exception异常类。

所以捕获一个特定类型的异常也将捕获它的子类的错误。 而且如果捕获了StandardError异常,像 ArgumentErrorIOErrorIndexError这些异常都将会被捕获,这显然不太好。

捕获所有异常

begin
  do_something()
rescue Exception => e
  ...
end

不要用上面的方法捕获异常,因为它将捕获所有类型的异常,程序也会出现奇怪的错误。

因为Ruby也会使用异常来捕获来自操作系统中称之为『信号』(Signals)的消息,像我们平时使用ctrl-c来退出程序也使用了一个信号,如果捕获所有的异常,也将捕获这些信号。

还有诸如语法错误这样的异常应该在语法错误时正确的抛出,如果被捕获到,将会找不到程序中到底是哪里出了错。

捕获所有错误

回头看异常对象层级,会发现所有应用级别的错误都继承自StandardError,所以如果我们想捕获所有应用级别的错误,都应该继承自StandardError

begin
  do_something()
rescue StandardError => e
  # 只有应用级别的错误会被捕获,SyntaxError 也会被排除
end

如果这里没有指定一个异常类,Ruby会假定这是一个StandardError类型的错误。

begin
  do_something()
rescue => e
  # 这里和指定了StandardError类型错误效果一样
end

捕获特定错误

虽然知道了如何捕获所有应用级错误,但这看起来也不是一个好的做法。

如果不定义一个特定的类型来捕获特定的错误,区别开那些不需要捕获的错误,他们很有可能会在后面给你找麻烦。

begin
  do_something()
rescue Errno::ETIMEDOUT => e
  # 这里只会捕获 Errno::ETIMEDOUT 异常
end

还可以在一个区块中捕获多个异常:

begin
  do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end