17-異常處理
===========
[Errors](#171-errors)<br/>
[Throws](#172-throws)<br/>
[Exits](#173-exits)<br/>
[After](#174-after)<br/>
[變量作用域](#175-%E5%8F%98%E9%87%8F%E4%BD%9C%E7%94%A8%E5%9F%9F)<br/>
Elixir有三種錯誤處理機制:errors,throws和exits。本章我們將逐個講解它們,包括應該在何時使用哪一個。
## 17.1-Errors
舉個例子,嘗試讓原子加上一個數字,就會激發一個錯誤(errors):
```
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```定義你自己的錯誤。最常見的是定義一個有消息說明的錯誤:
```
iex> defexception MyError, message: "default message"
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message
```
用```try/catch```結構可以處理異常:
```
iex> try do
...> raise "oops"
...> rescue
...> e in RuntimeError -> e
...> end
RuntimeError[message: "oops"]
```
這個例子處理了一個運行時異常,返回該錯誤本身(會被顯示在IEx對話中)。
在實際操作中,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 "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```是因為我們**不用錯誤處理來控制程序執行流程**。
在Elixir中,我們視錯誤為其字面意思:它們只不過是用來表示意外或異常的信息。
如果你真的希望改變執行過程,你可以使用```throws```。
## 17.2-Throws
在Elixir中,你可以拋出(throw)一個值稍后處理。```throw```和```catch```就被保留著為了處理一些你拋出了值,但是不用```try/catch```就取不到的情況。
這些情況實際中很少出現,除非當一個庫的接口沒有提供合適的API等情況。
例如,假如枚舉模塊沒有提供任何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-Exits
每段Elixir代碼都在進程中運行,進程與進程相互交流。當一個進程終止了,它會發出```exit```信號。
一個進程可以通過顯式地發出這個信號來終止:
```
iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 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信號就更少見了。
exit信號是Erlang虛擬機提供的高容錯性的重要部分。進程通常都在*監督樹(supervision trees)*下運行。
監督樹本身也是進程,它們通過exit信號監督其它進程。然后通過某些策略決定是否重啟。
就是這種監督系統使得```try/catch```和```try/rescue```代碼塊很少用到。與其處理一個錯誤,不如讓它*快速失敗*。
因為在失敗后,監督樹會保證我們的程序將恢復到一個已知的初始狀態去。
## 17.4-After
有時候有必要使用```try/after```來保證某資源在使用后被正確關閉或清除。
例如,我們打開一個文件,然后使用```try/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
```
## 17.5-變量作用域
對于定義在```try/catch/rescue/after```代碼塊中的變量,切記不可讓它們泄露到外面去。這時因為```try```代碼塊有可能會失敗,而這些變量此時并沒有正常綁定數值:
```
iex> try do
...> from_try = true
...> after
...> from_after = true
...> end
iex> from_try
** (RuntimeError) undefined function: from_try/0
iex> from_after
** (RuntimeError) undefined function: from_after/0
```
至此我們結束了對```try/catch/rescue```等知識的介紹。你會發現其實這些概念在實際的Elixir編程中不太常用。盡管的確有時也會用到。
是時候討論一些Elixir的概念,如列表速構(comprehensions)和魔法印(sigils)了。