Skip to content

Commit 5fa96ba

Browse files
committed
update try-rescue
1 parent afe6804 commit 5fa96ba

File tree

1 file changed

+164
-67
lines changed

1 file changed

+164
-67
lines changed

17-try-catch.md

Lines changed: 164 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,130 @@
11
17-异常处理
22
===========
3-
[Errors](#171-errors)<br/>
4-
[Throws](#172-throws)<br/>
5-
[Exits](#173-exits)<br/>
6-
[After](#174-after)<br/>
7-
[变量作用域](#175-%E5%8F%98%E9%87%8F%E4%BD%9C%E7%94%A8%E5%9F%9F)<br/>
83

9-
Elixir有三种错误处理机制:errors,throws和exits。本章我们将逐个讲解它们,包括应该在何时使用哪一个。
4+
Elixir有三种错误处理机制:errors,throws和exits。
5+
本章我们将逐个讲解它们,包括应该在何时使用哪一个。
106

11-
## 17.1-Errors
12-
举个例子,尝试让原子加上一个数字,就会激发一个错误(errors):
13-
```
7+
## Errors
8+
9+
错误(errors,或者叫它异常)用在代码中出现意外的地方。
10+
举个例子,尝试让原子加上一个数字,就会返回一个错误:
11+
12+
```elixir
1413
iex> :foo + 1
1514
** (ArithmeticError) bad argument in arithmetic expression
1615
:erlang.+(:foo, 1)
1716
```
1817

19-
使用宏```raise/1```可以在任何时候激发一个运行时错误:
20-
```
18+
`raise/1`可以在任何时候激发一个运行时错误:
19+
20+
```elixir
2121
iex> raise "oops"
2222
** (RuntimeError) oops
2323
```
2424

25-
```raise/2```,并且附上错误名称和一个键值列表可以激发规定好的错误:
26-
```
25+
也可以调用`raise/2`抛出错误,并且附上错误名称和一个键值列表:
26+
27+
```elixir
2728
iex> raise ArgumentError, message: "invalid argument foo"
2829
** (ArgumentError) invalid argument foo
2930
```
3031

31-
你可以使用```defexception/2```定义你自己的错误。最常见的是定义一个有消息说明的错误:
32-
```
33-
iex> defexception MyError, message: "default message"
32+
你可以定义一个模块,在里面使用`defexception/2`定义你自己的错误。
33+
这种方式,你创建的错误和模块同名。最常见的是定义一个有详细错误信息字段的错误:
34+
35+
```elixir
36+
iex> defmodule MyError do
37+
iex> defexception message: "default message"
38+
iex> end
3439
iex> raise MyError
3540
** (MyError) default message
3641
iex> raise MyError, message: "custom message"
3742
** (MyError) custom message
3843
```
3944

40-
```try/catch```结构可以处理异常:
41-
```
45+
错误可以用`try/catch`结构 **拯救(rescued)**
46+
47+
```elixir
4248
iex> try do
4349
...> raise "oops"
4450
...> rescue
4551
...> e in RuntimeError -> e
4652
...> end
47-
RuntimeError[message: "oops"]
53+
%RuntimeError{message: "oops"}
4854
```
49-
这个例子处理了一个运行时异常,返回该错误本身(会被显示在IEx对话中)。
50-
在实际操作中,Elixir程序员很少使用```try/rescue```结构。
51-
例如,当文件打开失败,很多编程语言会强制你去处理一个异常。而Elixir提供的```File.read/1```函数返回包含信息的元组,不管文件打开成功与否:
55+
56+
这个例子处理了一个运行时异常,返回该错误本身(被显示在`:iex`回话中)。
57+
58+
如果你不需要使用错误对象,你可以不提供它:
59+
60+
```elixir
61+
iex> try do
62+
...> raise "oops"
63+
...> rescue
64+
...> RuntimeError -> "Error!"
65+
...> end
66+
"Error!"
5267
```
68+
69+
在实际应用中,Elixir程序员极少使用`try/rescue`结构。
70+
例如,当文件打开失败,很多编程语言会强制你去处理异常。
71+
而Elixir提供的`File.read/1`函数不管文件打开成功与否,都会返回包含结果信息的元组:
72+
73+
```elixir
5374
iex> File.read "hello"
5475
{:error, :enoent}
5576
iex> File.write "hello", "world"
5677
:ok
5778
iex> File.read "hello"
5879
{:ok, "world"}
5980
```
60-
这个例子中没有```try/rescue```。如果你想处理打开文件可能的不同结果,你可以使用case来匹配:
61-
```
81+
82+
这个例子中没有用到`try/rescue`
83+
如果你想处理打开文件可能发生的不同结果,你可以使用`case`结构来做模式匹配:
84+
85+
```elixir
6286
iex> case File.read "hello" do
63-
...> {:ok, body} -> IO.puts "got ok"
64-
...> {:error, body} -> IO.puts "got error"
87+
...> {:ok, body} -> IO.puts "Success: #{body}"
88+
...> {:error, reason} -> IO.puts "Error: #{reason}"
6589
...> end
6690
```
67-
使用这个匹配处理,你可以自己决定要不要把问题抛出来。
68-
这就是为什么Elixir不让```File.read/1```等函数自己抛出异常。它把决定权留给程序员,让他们寻找最合适的处理方法。
6991

92+
使用这种匹配处理,你就可以自己决定打开文件异常是不是一种错误。
93+
这也是为什么Elixir不让`File.read/1`等函数自己抛出异常。
94+
它把决定权留给程序员,让他们寻找最合适的处理方法。
7095

71-
如果你真的期待文件存在(_打开文件时文件不存在_这确实是一个错误),你可以简单地使用```File.read!/1```
72-
```
96+
如果你真的期待文件存在(_打开文件时文件不存在_ 确实是一个 **错误**),
97+
你可以简单地使用`File.read!/1`
98+
99+
```elixir
73100
iex> File.read! "unknown"
74101
** (File.Error) could not read file unknown: no such file or directory
75102
(elixir) lib/file.ex:305: File.read!/1
76103
```
77104

78-
换句话说,我们避免使用```try/rescue```是因为我们**不用错误处理来控制程序执行流程**
79-
在Elixir中,我们视错误为其字面意思:它们只不过是用来表示意外或异常的信息。
80-
如果你真的希望改变执行过程,你可以使用```throws```
105+
Many functions in the standard library follow the pattern of having a counterpart that raises an exception instead of returning tuples to match against. The convention is to create a function (foo) which returns {:ok, result} or {:error, reason} tuples and another function (foo!, same name but with a trailing !) that takes the same arguments as foo but which raises an exception if there’s an error. foo! should return the result (not wrapped in a tuple) if everything goes fine. The File module is a good example of this convention.
81106

82-
## 17.2-Throws
83-
在Elixir中,你可以抛出(throw)一个值稍后处理。```throw``````catch```就被保留着为了处理一些你抛出了值,但是不用```try/catch```就取不到的情况。
107+
但是标准库中的许多函数都遵循这样的模式:不返回元组来模式匹配,而是抛出异常。
108+
具体说,这种约定是在定义名字不带感叹号后缀的函数时,返回结果信息的元组;
109+
定义带有感叹号后缀的相同函数时,使用触发异常的机制。
110+
可以阅读`File`模块的代码,有很多关于这种约定的好例子。
84111

85-
这些情况实际中很少出现,除非当一个库的接口没有提供合适的API等情况。
86-
例如,假如枚举模块没有提供任何API来寻找某范围内第一个13的倍数:
87-
```
112+
总之在Elixir中,我们避免使用`try/rescue`是因为我们**不通过错误处理机制来控制程序执行流程**
113+
我们视错误为其字面意思:它们只不过是用来处理意外时用到的信息。
114+
如果你真的希望异常机制改变程序执行流程,可以使用`throws`
115+
116+
## Throws
117+
118+
在Elixir中,你可以抛出(throw)一个值(不一定是一个错误/异常对象)做后续处理并终止当前其它操作。
119+
(前方文字高能=>)`throw``catch`就被保留用来处理这样有值被抛出、但是不用`try/catch`就取不到的情况。
120+
121+
>译注:这句话原文也差不多。仔细读读感觉逻辑类似于“X被用来描述不用X就无法描述的东西”。
122+
123+
这中情况很不多,非要找得话,比如当一个库的接口没有提供合适的API时。
124+
举个例子,`Enum`模块没有提供现成的API来做这个奇葩的事情:寻找若干数字里第一个13的倍数。
125+
你可以利用`throw`机制,在刚找到第一个符合条件的数时,抛出这个数字,并终止执行流程:
126+
127+
```elixir
88128
iex> try do
89129
...> Enum.each -50..50, fn(x) ->
90130
...> if rem(x, 13) == 0, do: throw(x)
@@ -96,24 +136,33 @@ iex> try do
96136
"Got -39"
97137
```
98138

99-
但是它提供了这样的函数```Enum.find/2```
100-
```
139+
>但实际上`Enum`提供了这样的API---使用`Enum.find/2`
140+
141+
>```elixir
101142
iex> Enum.find -50..50, &(rem(&1, 13) == 0)
102143
-39
103144
```
104145
105-
## 17.3-Exits
106-
每段Elixir代码都在进程中运行,进程与进程相互交流。当一个进程终止了,它会发出```exit```信号。
107-
一个进程可以通过显式地发出这个信号来终止:
108-
```
146+
## Exits
147+
148+
Elixir代码都在各种进程中运行,彼此间相互通信。当一个进程因为自然原因死亡了(如“未处理的异常),
149+
它会发出`exit`信号。一个进程可以通过显式地发出`exit`信号来终止:
150+
151+
```elixir
109152
iex> spawn_link fn -> exit(1) end
110153
#PID<0.56.0>
111154
** (EXIT from #PID<0.56.0>) 1
112155
```
113-
上面的例子中,链接着的进程通过发送```exit```信号(带有参数数字1)而终止。Elixir shell自动处理这个信息并把它们显示在终端上。
114156

115-
exit还可以被```try/catch```块捕获处理:
116-
```
157+
上面的例子中,被链接的进程通过发送`exit`信号(带有参数1)而终止。
158+
Elixir shell自动处理这个消息并把它们显示在终端上。
159+
160+
>打比方,在某进程中执行的Elixir代码,突遇故障难以处理,便大喊一声“要死”后死亡;
161+
或者它主动喊出一声“要死”而死亡。是该进程发送消息(即,调用函数)。
162+
163+
`exit`可以被`try/catch`块捕获处理:
164+
165+
```elixir
117166
iex> try do
118167
...> exit "I am exiting"
119168
...> catch
@@ -122,18 +171,24 @@ iex> try do
122171
"not really"
123172
```
124173

125-
因为```try/catch```已经很少用了,用它们捕获exit信号就更少见了
174+
`try/catch`已经很少使用了,用它们捕获`exit`信号就更少见了
126175

127-
exit信号是Erlang虚拟机提供的高容错性的重要部分。进程通常都在*监督树(supervision trees)*下运行。
128-
监督树本身也是进程,它们通过exit信号监督其它进程。然后通过某些策略决定是否重启。
176+
`exit`信号是Erlang虚拟机提供的高容错性的重要部分。
177+
进程通常都在*监督树(supervision trees)*下运行。
178+
监督树本身也是进程,它任务就是一直等待下面被监督进程的`exit`信号。
179+
一旦监听到退出信号,监督策略就开始工作,发送了死亡信号的被监督进程会被重启。
129180

130-
就是这种监督系统使得```try/catch``````try/rescue```代码块很少用到。与其处理一个错误,不如让它*快速失败*
131-
因为在失败后,监督树会保证我们的程序将恢复到一个已知的初始状态去。
181+
就是这种监督机制使得`try/catch``try/rescue`代码块很少用到。
182+
与其拯救一个错误,不如让它*快速失败*
183+
因为在程序发生失败后,监督树会保证这个程序恢复到一个已知的初始状态去。
132184

133-
## 17.4-After
134-
有时候有必要使用```try/after```来保证某资源在使用后被正确关闭或清除。
135-
例如,我们打开一个文件,然后使用```try/after```来确保它在使用后被关闭:
136-
```
185+
## After
186+
187+
有时候我们有必要确保某资源在执行可能会发生错误的操作后被正确地关闭或清理。
188+
使用`try/after`结构来做这个。
189+
例如打开一个文件,使用`after`子句来确保它在使用后被关闭,即使发生了错误:
190+
191+
```elixir
137192
iex> {:ok, file} = File.open "sample", [:utf8, :write]
138193
iex> try do
139194
...> IO.write file, "olá"
@@ -144,20 +199,62 @@ iex> try do
144199
** (RuntimeError) oops, something went wrong
145200
```
146201

147-
## 17.5-变量作用域
148-
对于定义在```try/catch/rescue/after```代码块中的变量,切记不可让它们泄露到外面去。这时因为```try```代码块有可能会失败,而这些变量此时并没有正常绑定数值:
202+
`after`块中的代码,无论`try`代码块中是否出现错误,其都会执行。
203+
但是,也有例外。如果一个链接进程整个终止了,它代码中的`after`块可能还没有执行。
204+
因此,我们说`after`只是个“软”保障。
205+
幸运的是,在Elixir语言中,打开的文件永远是链接到当前进程的。如果当前进程挂了,文件也会跟着关闭。
206+
不管执没执行`after`块。
207+
其它一些资源,如ETS表,套接字,端口等也是这样。
208+
209+
有时候你想将整个函数体用`try`结构包起来,是某些代码在某情况下会后续执行。
210+
这时,Elixir允许你不写`try`那行:
211+
212+
```elixir
213+
iex> defmodule RunAfter do
214+
...> def without_even_trying do
215+
...> raise "oops"
216+
...> after
217+
...> IO.puts "cleaning up!"
218+
...> end
219+
...> end
220+
iex> RunAfter.without_even_trying
221+
cleaning up!
222+
** (RuntimeError) oops
149223
```
224+
225+
这时,Elixir会将整个函数体的代码用`try`块包裹,不管后面是`after``rescue`还是`catch`
226+
227+
## 变量的作用域
228+
229+
定义在`try/catch/rescue/after`代码块中的变量,不会泄露到外面去。
230+
这是因为`try`代码块有可能会失败,而这些变量此时并没有正常绑定数值:
231+
232+
换句话说,下面这份代码是错误的:
233+
234+
```elixir
150235
iex> try do
151-
...> from_try = true
152-
...> after
153-
...> from_after = true
236+
...> raise "fail"
237+
...> what_happened = :did_not_raise
238+
...> rescue
239+
...> _ -> what_happened = :rescued
154240
...> end
155-
iex> from_try
156-
** (RuntimeError) undefined function: from_try/0
157-
iex> from_after
158-
** (RuntimeError) undefined function: from_after/0
241+
iex> what_happened
242+
** (RuntimeError) undefined function: what_happened/0
159243
```
160244

161-
至此我们结束了对```try/catch/rescue```等知识的介绍。你会发现其实这些概念在实际的Elixir编程中不太常用。尽管的确有时也会用到。
245+
相反,你可以保存`try`表达式的返回值:
246+
247+
```elixir
248+
iex> what_happened =
249+
...> try do
250+
...> raise "fail"
251+
...> :did_not_raise
252+
...> rescue
253+
...> _ -> :rescued
254+
...> end
255+
iex> what_happened
256+
:rescued
257+
```
162258

163-
是时候讨论一些Elixir的概念,如列表速构(comprehensions)和魔法印(sigils)了。
259+
至此我们结束了对`try/catch/rescue`的介绍。
260+
你会发现Elixir语言中的这些概念的使用频率比其他语言小,尽管的确有时使用起来也挺顺手。

0 commit comments

Comments
 (0)