尝试,捕捉和拯救

Elixir 有三种错误机制:错误,引发和退出。在本章中,我们将探讨它们中的每一个,并且包括关于每个应该被使用的时间的评论。

错误(异常)

在代码中发生异常情况时会使用错误(或异常)。通过尝试将一个数字添加到原子中可以检索出一个示例错误:

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)

运行时错误可以随时通过使用raise/1

iex> raise "oops"
** (RuntimeError) oops

raise/2通过传递错误名称和关键字参数列表可以引发其他错误:

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

您也可以通过创建一个模块并使用其中的defexception构造来定义自己的错误; 这样,您将创建一个与它所定义的模块同名的错误。最常见的情况是使用消息字段定义自定义异常:

iex> defmodule MyError do
iex>   defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

错误可以使用这个构造来拯救try/rescue

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

上面的例子挽救了运行时错误,并返回了错误本身,然后打印在iex会话中。

如果您对该错误没有任何用处,则不必提供:

iex> try do
...>   raise "oops"
...> rescue
...>   RuntimeError -> "Error!"
...> end
"Error!"

然而,在实践中,Elixir开发者很少使用这个try/rescue构造。例如,许多语言会在文件无法成功打开时强制您解决错误。Elixir 反而提供了一个File.read/1函数,它返回一个包含有关文件是否被成功打开的信息的元组:

iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
:ok
iex> File.read "hello"
{:ok, "world"}

这里没有try/rescue。如果你想处理打开文件的多个结果,你可以在case结构中使用模式匹配:

iex> case File.read "hello" do
...>   {:ok, body}      -> IO.puts "Success: #{body}"
...>   {:error, reason} -> IO.puts "Error: #{reason}"
...> end

在一天结束时,应由您的应用程序来决定打开文件时出现的错误是否例外。这就是为什么 Elixir 不会在File.read/1其他许多功能上施加例外。相反,它会让开发人员选择最佳的处理方式。

对于您希望文件存在的情况(并且缺少该文件确实是错误),您可以使用File.read!/1

iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
    (elixir) lib/file.ex:272: File.read!/1

标准库中的许多函数遵循引发异常而不是返回元组进行匹配的对应方式。约定是创建一个函数(foo),它返回{:ok,result}{:error,reason}元组以及另一个函数(foo!同名但有尾!),它们采用相同的参数,foo但如果出现错误则会引发异常。foo!如果一切正常,应该返回结果(不包含在一个元组中)。File模块是这个约定的一个很好的例子。

在 Elixir 中,我们避免使用,try/rescue因为我们不使用控制流的错误。我们从字面上理解错误:它们被保留用于意外和/或特殊情况。如果你真的需要流量控制结构,应该使用抛出。这就是我们接下来要看到的。

抛出

在 Elixir 中,可以抛出一个值并在以后被捕获。throwcatch保留用于不可能检索到值的情况,除非使用throwcatch

这些情况在实践中很少见,除非与不提供适当 API 的库进行连接。例如,让我们假设该Enum模块没有提供任何用于查找值的 API,并且我们需要在数字列表中找到13的第一个倍数:

iex> try do
...>   Enum.each -50..50, fn(x) ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

既然Enum 确实提供了一个合适的 API,实际上Enum.find/2就是要走的路:

iex> Enum.find -50..50, &(rem(&1, 13) == 0)
-39

退出

所有Elixir代码运行在相互通信的进程中。当一个进程死于“自然原因”(例如,未处理的异常)时,它会发送一个exit信号。一个进程也可以通过明确发送一个退出信号而死亡:

iex> spawn_link fn -> exit(1) end
** (EXIT from #PID<0.56.0>) evaluator process exited with reason: 1

在上面的示例中,链接的进程通过发送exit值为1 的信号而死亡。Elixir shell 自动处理这些消息并将其打印到终端。

exit也可以使用try/catch以下方式“抓住” :

iex> try do
...>   exit "I am exiting"
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

使用try/catch已经非常普遍,使用它来获取出口更为罕见。

exit信号是 Erlang VM 提供的容错系统的重要组成部分。过程通常在监督树下运行,监督树本身就是监听exit监督过程信号的过程。一旦收到退出信号,监督策略开始并且监督过程重新开始。

正是这种监督体系,使得结构,如try/catchtry/rescue在药剂等等屡见不鲜。由于监督树会保证我们的应用程序在错误发生后回到已知的初始状态,因此我们宁愿“fail fast”,而不是拯救错误。

之后

有时需要确保在某些可能引发错误的操作之后清理资源。该try/after构造允许你这样做。例如,我们可以打开一个文件并使用一个after子句来关闭它 - 即使出现问题:

iex> {:ok, file} = File.open "sample", [:utf8, :write]
iex> try do
...>   IO.write file, "olá"
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

after无论 try 块是否成功,该子句都将被执行。但是,请注意,如果链接进程退出,则此进程将退出并且该after子句不会运行。因此after只提供了一个软性保证。幸运的是,Elixir 中的文件也与当前进程相关联,因此如果当前进程崩溃,它们将始终关闭,而与after子句无关。你会发现 ETS 表,套接字,端口等其他资源也是如此。

有时你可能想要将一个函数的整个主体包装在一个try构造中,通常可以保证一些代码将在之后执行。在这种情况下,Elixir 允许您省略该try行:

iex> defmodule RunAfter do
...>   def without_even_trying do
...>     raise "oops"
...>   after
...>     IO.puts "cleaning up!"
...>   end
...> end
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops

酏剂会自动换函数体在try每当一个afterrescuecatch指定。

其他

如果存在else块,则try只要try块完成而没有抛出或错误,它就会匹配块的结果。

iex> x = 2
2
iex> try do
...>   1 / x
...> rescue
...>   ArithmeticError ->
...>     :infinity
...> else
...>   y when y < 1 and y > -1 ->
...>     :small
...>   _ ->
...>     :large
...> end
:small

else块中的例外情况未被捕获。如果else块内没有模式匹配,则会引发异常;这个异常没有被当前try/catch/rescue/after块捕获。

变量范围

请牢记try/catch/rescue/after块内定义的变量不会泄漏到外部环境中,这一点很重要。这是因为该try块可能会失败,因此变量可能永远不会被绑定在首位。换句话说,这段代码是无效的:

iex> try do
...>   raise "fail"
...>   what_happened = :did_not_raise
...> rescue
...>   _ -> what_happened = :rescued
...> end
iex> what_happened
** (RuntimeError) undefined function: what_happened/0

相反,您可以存储try表达:

iex> what_happened =
...>   try do
...>     raise "fail"
...>     :did_not_raise
...>   rescue
...>     _ -> :rescued
...>   end
iex> what_happened
:rescued

这完成了我们对trycatchrescue的介绍。你会发现Elixir中使用它的频率比其他语言少,尽管在某些情况下图书馆或某些特定的代码不按“规则”播放。