17 try, catch和rescue
Elixir有三个错误处理机制:错误,抛出和退出。在这一章我们将一一探索它们,包括应该在什么时候使用它们。
17.1 错误
第一个典型的错误是试图把一个数字和原子相加:
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/2定义你自己的错误。最常见的例子是定义一个带有message域的异常:
iex> defexception MyError, message: "default message"iex> raise MyError** (MyError) default messageiex> raise MyError, message: "custom message"** (MyError) custom message
异常可以被通过try/rescue挽救:
iex> try do...> raise "oops"...> rescue...> e in RuntimeError -> e...> endRuntimeError[message: "oops"]
上面的例子挽救了一个运行时错误并返回这个错误,并在iex的session中打印出来。在实践中Elixir开发者很少使用try/rescue结构。例如,许多语言中但一个文件无法被打开时,会强制你去挽救一个错误。相反Elixir提供了一个函数File.read/1,它返回一个包含文件是否被成功打开的相关信息的元组。
iex> File.read "hello"{:error, :enoent}iex. File.write "hello", "world":okiex> File.read "hello"{:ok, "world"}
这里没有try/rescue。如果你想要处理开打文件的不同后果,你可以非常方便地使用case做模式识别:
iex> case File.read "hello" do...> {:ok, body} -> IO.puts "got ok"...> {:error, body} -> IO.puts "got error"...> 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:305: File.read!/1
用另一种话来说,我们避免使用try/rescue因为我们不用错误来做控制流程。在Eliixr中,我们对错误的理解是字面意义上的:它们就是没有预期的或留给意外的或情况的。如果你的确需要流程控制结构,就需要用到throw。这也是下一章的内容。
17.2 抛出
在Elixir中,一个抛出的值能之后被捕获。throw和catch是用于除了是用throw和catch之外没法获取一个值的情况。
这些情况在时间中是不常见的,除非当你要和一些API定义地不好的库打交道的时候。例如,让我们想象v模块没有提供提供寻找值的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.find/2就可以轻松做到:
iex> Enum.find -50..50, &(rem(&1, 13) == 0)-39
17.3 退出
所有的Eliixr代码都运行在进程中,进程之间互相通信。当一个进程死亡,它会发送一个exit信号。也可以手动发送一个退出信号来杀死一个进程:
iex> spawn_link fn -> exit(1) end#PID<0.56.0>** (EXIT from #PID<0.56.0>) 1
在上面的例子中,我们链接了死亡的进程之后发送了退出信号,它的值是1.Elixir控制台自动处理这些消息,并把它们打印出来:
exit也能被``捕获:
iex> try do...> exit "I am exiting"...> catch...> :exit, _ -> "not really"...> end"not really"
用try/catch已经是非常罕见的,用它们来捕获退出更是少见。
exit信号是Eralng虚拟机提供的tolerant机制的重要组成部分。进程通常在监控树之下运行,它们其实是一个等待被监控的进程的退出信号的进程。当一个收到一个退出信号,它们的监控策略被触发并将死亡的被监控进程重启。
正式这个监控系统使得try/catch和try/rescue在Elixir中用的这么少。与其挽救一个错误,我们更愿意让他先失败,因为监控树会保证我们的应用在错误之后,会重回到一个已知的初始状态。
17.4 之后
有时的确有必要时候try/after来保证一个资源在某些特定的动作之后被清理。例如,我们打开了一个文件并用try/after来保证它的关闭。
iex> {:ok, file} = File.open "sample", [:utf8, :write]iex> try do...> IO.write file, "josé"...> raise "oops, something went wrong"...> after...> File.close(file)...> end** (RuntimeError) oops, something went wrong
17.5 变量作用域
牢记在try/catch/rescue/after内部定义的变量并不会泄漏到外部环境中。这是因为try块也会会失败,所以变量也许从一开始就是找不到的。换一句话来说,这些代码是非法的:
iex> try do...> from_try = true...> after...> from_after = true...> endiex> from_try** (RuntimeError) undefined function: from_try/0iex> from_after** (RuntimeError) undefined function: from_after/0
到这里,我们结束了我们对try,catch和rescue的介绍。你会发现和在其他语言中相比,它们在Elixir中用的不多。当然在某些特定的情况下当一个库或一些特定的代码“不按规矩出牌”的时候,也许它们会有用。
是时候让我们谈谈一些Elixir的构建比如comprehension和sigil了。
