跳至内容

异常处理

Crystal 处理错误的方式是通过引发和捕获异常。

引发异常

您可以通过调用顶级 raise 方法来引发异常。与其他关键字不同,raise 是一个具有两个重载的常规方法: 一个接受字符串,另一个 接受异常实例

raise "OH NO!"
raise Exception.new("Some error")

字符串版本只是使用该消息创建一个新的 Exception 实例。

只有 Exception 实例或子类可以被引发。

定义自定义异常

要定义自定义异常类型,只需从 Exception 派生。

class MyException < Exception
end

class MyOtherException < Exception
end

与往常一样,您可以为异常定义一个构造函数,也可以使用默认构造函数。

捕获异常

要捕获任何异常,请使用 begin ... rescue ... end 表达式

begin
  raise "OH NO!"
rescue
  puts "Rescued!"
end

# Output: Rescued!

要访问被捕获的异常,可以在 rescue 子句中指定一个变量

begin
  raise "OH NO!"
rescue ex
  puts ex.message
end

# Output: OH NO!

要捕获只是一种类型的异常(或其任何子类)

begin
  raise MyException.new("OH NO!")
rescue MyException
  puts "Rescued MyException"
end

# Output: Rescued MyException

有效的类型限制是 ::Exception 的子类,模块类型和它们的联合。

要访问它,请使用类似于类型限制的语法

begin
  raise MyException.new("OH NO!")
rescue ex : MyException
  puts "Rescued MyException: #{ex.message}"
end

# Output: Rescued MyException: OH NO!

可以指定多个 rescue 子句

begin
  # ...
rescue ex1 : MyException
  # only MyException...
rescue ex2 : MyOtherException
  # only MyOtherException...
rescue
  # any other kind of exception
end

您还可以通过指定联合类型来一次捕获多个异常类型

begin
  # ...
rescue ex : MyException | MyOtherException
  # only MyException or MyOtherException
rescue
  # any other kind of exception
end

else

只有在没有捕获异常的情况下才执行 else 子句

begin
  something_dangerous
rescue
  # execute this if an exception is raised
else
  # execute this if an exception isn't raised
end

只有在指定了至少一个 rescue 子句的情况下才能指定 else 子句。

ensure

ensure 子句在 begin ... endbegin ... rescue ... end 表达式的末尾执行,无论是否引发了异常

begin
  something_dangerous
ensure
  puts "Cleanup..."
end

# Will print "Cleanup..." after invoking something_dangerous,
# regardless of whether it raised or not

或者

begin
  something_dangerous
rescue
  # ...
else
  # ...
ensure
  # this will always be executed
end

ensure 子句通常用于清理,释放资源等。

简短语法形式

异常处理有简短的语法形式:假设方法或块定义是一个隐式 begin ... end 表达式,然后指定 rescueelseensure 子句

def some_method
  something_dangerous
rescue
  # execute if an exception is raised
end

# The above is the same as:
def some_method
  begin
    something_dangerous
  rescue
    # execute if an exception is raised
  end
end

使用 ensure

def some_method
  something_dangerous
ensure
  # always execute this
end

# The above is the same as:
def some_method
  begin
    something_dangerous
  ensure
    # always execute this
  end
end

# Similarly, the shorthand also works with blocks:
(1..10).each do |n|
  # potentially dangerous operation


rescue
  # ..
else
  # ..
ensure
  # ..
end

类型推断

在异常处理程序的 begin 部分中声明的变量在 rescueensure 体中被视为 Nil 类型。例如

begin
  a = something_dangerous_that_returns_Int32
ensure
  puts a + 1 # error, undefined method '+' for Nil
end

即使 something_dangerous_that_returns_Int32 从未引发,或者 a 被赋值,然后执行了可能引发异常的方法,也会发生上述情况

begin
  a = 1
  something_dangerous
ensure
  puts a + 1 # error, undefined method '+' for Nil
end

尽管很明显 a 始终会被赋值,但编译器仍然认为 a 可能没有机会被初始化。虽然这种逻辑将来可能会得到改进,但目前它迫使您将异常处理程序保持在必要的最小范围内,使代码的意图更加清晰

# Clearer than the above: `a` doesn't need
# to be in the exception handling code.
a = 1
begin
  something_dangerous
ensure
  puts a + 1 # works
end

处理错误的替代方法

虽然异常是处理错误的机制之一,但它们不是您的唯一选择。引发异常涉及分配内存,执行异常处理程序通常很慢。

标准库通常提供一些方法来完成某事:一个引发,一个返回 nil。例如

array = [1, 2, 3]
array[4]  # raises because of IndexError
array[4]? # returns nil because of index out of bounds

通常的做法是提供一个替代的“问题”方法来表示该方法的这种变体返回 nil 而不是引发异常。这允许用户选择是处理异常还是处理 nil。但是请注意,这并不适用于所有方法,因为异常仍然是首选方式,因为它们不会用错误处理逻辑污染代码。