以DRY的方式在Ruby的rescue子句中传递多个错误类

125

我有一些需要在Ruby中捕获多种类型异常的代码:

begin
  a = rand
  if a > 0.5
    raise FooException
  else
    raise BarException
  end
rescue FooException, BarException
  puts "rescued!"
end

我想要做的是将我想要捕获的异常类型列表存储在某处,并将这些类型传递给rescue子句:

EXCEPTIONS = [FooException, BarException]

然后:

rescue EXCEPTIONS

这是否可能,而且是否可以避免使用一些非常hack的eval调用?我并不抱太大希望,因为当我尝试上述操作时,出现了TypeError: class or module required for rescue clause


2
关于异常处理,有什么建议吗? - Roman
3个回答

237
你可以使用带有扩展运算符*的数组。
EXCEPTIONS = [FooException, BarException]

begin
  a = rand
  if a > 0.5
    raise FooException
  else
    raise BarException
  end
rescue *EXCEPTIONS
  puts "rescued!"
end
如果您要像上面使用常量数组(如EXCEPTIONS),请注意您不能在定义内部定义它,而且如果您在其他类中定义它,则必须使用其命名空间引用它。实际上,它不一定是一个常量。
散装运算符*会将数组“展开”到其位置,因此
rescue *EXCEPTIONS

意思相同

rescue FooException, BarException

你也可以在数组字面量内使用它,如下:

[BazException, *EXCEPTIONS, BangExcepion]

这与

[BazException, FooException, BarException, BangExcepion]

或者在参数位置中

method(BazException, *EXCEPTIONS, BangExcepion)

这意味着

method(BazException, FooException, BarException, BangExcepion)

[] 扩展为空集:

[a, *[], b] # => [a, b]

Ruby 1.8 和 Ruby 1.9 之间的一个区别是针对 nil 的处理。

[a, *nil, b] # => [a, b]       (ruby 1.9)
[a, *nil, b] # => [a, nil, b]  (ruby 1.8)

要小心具有 to_a 定义的对象,因为在这种情况下将应用 to_a

[a, *{k: :v}, b] # => [a, [:k, :v], b]

对于其他类型的对象,它返回自身。

[1, *2, 3] # => [1, 2, 3]

2
这似乎在ruby 1.8.7中也可以工作。在这种情况下,使用“*”字符在EXCEPTIONS前面的术语是什么?想学习更多。 - apb
2
@Andy,这被称为splat。它通常会将数组分解为逗号分隔的对象。当在方法定义的参数接收位置中使用时,它会执行相反的操作:将参数组合成一个数组。在各种场合中非常有用。很高兴知道它可以在1.8.7中使用。我已经相应地编辑了我的答案。 - sawa
22
请注意,如果您想要访问异常实例,请使用以下语法:rescue InvalidRequestError, CardError => e(参见http://mikeferrier.com/2012/05/19/rescuing-multiple-exception-types-in-ruby-and-binding-to-local-variable/)。 - Peter Ehrlich
4
这个语法是完全正常的:rescue *EXCEPTIONS => e,其中EXCEPTIONS是一个异常类名的数组。 - aks

13

编辑 / 更新

我完全错过了原问题的重点。
虽然被接受的答案是有效的,但在我看来,使用建议的技术不是一个好的做法。人们可以始终使用所需(且通用的)try/rescue包装函数。


虽然@sawa提供的答案在技术上是正确的,但我认为它滥用了Ruby的异常处理机制。

正如Peter Ehrlich的评论所建议的(通过指向Mike Ferrier的旧博客文章),Ruby已经配备了DRY异常处理机制:

puts 'starting up'
begin
  case rand(3)
  when 0
    ([] + '')
  when 1
    (foo)
  when 2
    (3 / 0)
  end
rescue TypeError, NameError => e
  puts "oops: #{e.message}"
rescue Exception => e
  puts "ouch, #{e}"
end
puts 'done'

通过使用这种技术,我们可以访问异常对象,通常它包含了一些有价值的信息。


1
-1 这是 OP 在他的问题中提到的语法。他想要将该列表提取到一个更易于维护的位置,我认为这是因为相同的异常类型列表必须在多个地方重复出现,并且对列表的更新容易出错。 - Segfault
2
只有在阅读了您的评论之后,我才终于理解了OP的问题。 - Ron Klein

2

我遇到了这个问题,并找到了另一种解决方案。如果你的FooExceptionBarException都将是自定义异常类,特别是如果它们都有主题相关性,你可以构建继承层次结构,使它们都继承自同一个父类,然后仅捕获父类。

例如,我有三个异常:FileNamesMissingErrorInputFileMissingErrorOutputDirectoryError,我想用一条语句来捕获它们。我又创建了另一个异常类FileLoadError,然后设置以上三个异常类都继承自它。之后,我只捕获了FileLoadError

像这样:

class FileLoadError < StandardError
end

class FileNamesMissingError < FileLoadError
end

class InputFileMissingError < FileLoadError
end

class OutputDirectoryError < FileLoadError
end

[FileNamesMissingError,
 InputFileMissingError,
 OutputDirectoryError].each do |error| 
   begin  
     raise error
   rescue FileLoadError => e
     puts "Rescuing #{e.class}."
   end 
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接