Elixir入门教程-尝试、捕获和挽救

  1. Errors
  2. Throws
  3. Exits
  4. After
  5. Else
  6. 变量作用域

Elixir有三种错误机制:errors,throws,和 exits 。本章我们将探索它们每一个,并且包括应该何时使用哪一个的评论。

Errors

错误(或者叫异常)是在代码里有异常的事情发生的时候被使用的。例如,通过尝试将一个数字和原子相加就可以获得一个错误:

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

任何时候用 raise/1 一个运行时错误可以被抛出:

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

其他种类的错误可以通过传入错误名字和一个关键字列表作为入参给函数 raise/2 来抛出:

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

通过创建一个模块并在它里面使用 defexception 结构,你也可以定义自己的错误;用这种方式,你将创建一个和这个错误定义所在的模块的名字相同的错误。最常见的场景是定义一个自定义的异常,并且有一个消息字段:

1
2
3
4
5
6
7
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 结构来挽救

1
2
3
4
5
6
iex> try do
...> raise "oops"
...> rescue
...> e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

上面的例子挽救了运行时错误并且返回这个错误,然后这个错误被打印在iex会话里。

如果错误对你无用,你就不需要返回它:

1
2
3
4
5
6
iex> try do
...> raise "oops"
...> rescue
...> RuntimeError -> "Error!"
...> end
"Error!"

但是在实践中,Elixir开发者很少使用 try/rescue 结构。例如,当一个文件不能被成功打开的时候,许多语言会强制要求你去挽救这个错误。Elixir反而是提供了一个函数 File.read/1 ,它返回一个元组,包含了关于这个文件是否被成功打开的相关消息:

1
2
3
4
5
6
iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
:ok
iex> File.read "hello"
{:ok, "world"}

这里就没有 try/rescue 。如果你想要处理打开一个文件的不同输出,你可以在case语句里使用模式匹配:

1
2
3
4
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 :

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

标准库里的许多函数遵循这样的模式:它有一个副本函数,它抛出异常而不是返回用来匹配的元组。这个约定习惯是,创建一个返回 {:ok, result} 或 {:error, reason} 元组的函数(foo)而另一个函数(foo!,相同的名字不过尾部有一个!字符)接收和foo函数一样的入参,但是如果有错误的时候,它抛出一个异常。如果一切正常,foo!将返回结果(而不是用元组包裹起来)。File模块是这个约定习惯的很好的例子。

在Elixir里,我们避免使用 try/rescue ,因为我们不使用错误来进行流程控制。我们真正对待错误的看法是:他们是为意外和/或例外情况预留的。如果你确实需要流程控制结构,可以使用 throws 。它就是我们接下来看到的。

Throws

在Elixir里,一个值可以被抛出然后被捕获。throw 和 catch 是为这样的情况保留的,就是:除了用throw 和 catch,不可能来获取到一个值。

实际上这些场景是非常罕见的,除非当你和没有提供合适API的库交互的时候。例如,让我们假设Enum模块没有提供任何API来找到一个值,而这个值就是我们需要在一个数字列表里找到第一个13的倍数的数字:

1
2
3
4
5
6
7
8
9
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是最好的人选:

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

Exits

所有Elixir代码运行在彼此交互的进程里。当一个进程死于“自然原因”(比如,没有处理异常),它会发送一个退出信号。一个进程也可以通过明确地发送一个退出信号而死亡:

1
2
3
iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 1

上面的例子,通过发送值为1的退出信号,被链接的进程死亡。Elixir shell自动处理那些消息并打印在终端上。

退出可以用try/catch来“捕获”:

1
2
3
4
5
6
iex> try do
...> exit "I am exiting"
...> catch
...> :exit, _ -> "not really"
...> end
"not really"

使用 try/catch 已经很少见了,用它来捕获退出更加罕见。

退出信号是由Erlang虚拟机提供的容错系统的一个重要部分。进程常常运行在监督树下,监督树也是进程;这些进程监听从被监督进程来的退出信号。一旦接收到一个退出信号,监督策略被触发,被监督进程被重启。

正是这种监督体系,使得像 try/catch 和 try/rescue 这样的结构在Elixir里那么罕见。我们宁愿“速错”而不是挽救一个错误,因为我们的监督树将保证我们的应用在错误后将回到一个可知的初始状态。

After

有时候,在一些可能潜在地引起错误的动作后确保资源被清理干净是必须的。try/after 结构允许你这么做。例如,我们可以打开一个文件并使用after分支关闭它,即使有一些事情出错:

1
2
3
4
5
6
7
8
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表、socket、port等等也是这样。

有时你可能想在一个try结构中封装一个函数的整个身体部分,这样通常是为了保证一些代码在后面被执行。这样的场景,Elixir允许你省略 try 这一行:

1
2
3
4
5
6
7
8
9
10
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

任何时候,after、rescue 或 catch 其中任何一个被用到了,Elixir将自动地封装函数的身体部分到try结构里。

Else

提供一个 else 块来允许在表达式执行结果上进行模式匹配。

1
2
3
4
5
6
7
8
9
10
11
12
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end

结果被传递给else,并且在那里陪匹配。请注意,如果一个异常被捕获,else也被执行,catch/rescue 块的结果将传递给else。

else块的异常不被捕获。如果else块里的模式没有被匹配上,一个异常将被抛出,这个异常不被当前的 try/catch/rescue/after 块捕获。

变量作用域

重要的是要记住,定义在try/catch/rescue/after块内的变量不泄漏到外部环境。这是因为try块可能失败,并且变量可能在第一个地方没有被绑定。换句话说,下面的代码是无效的:

1
2
3
4
5
6
7
8
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表达式的值:

1
2
3
4
5
6
7
8
9
iex> what_happened =
...> try do
...> raise "fail"
...> :did_not_raise
...> rescue
...> _ -> :rescued
...> end
iex> what_happened
:rescued

到此我们结束了try、catch 和 rescue的介绍。你将发现它们在Elixir里用得频率比在其他语言里少很多,虽然在一些库或某些特定的代码不“按规则”玩的情况下,它们用起来可能很方便。

原文链接: http://elixir-lang.org/getting-started/try-catch-and-rescue.html