Catch和Throw
Catch和Throw
catch和throw提供了一种表达式求值的监视机制,可以用于- 处理顺序代码中的错误(catch)- 函数的非本地返回(catch结合throw)表达式求值失败(如一次匹配失败)的一般后果是导致求值进程的异常退出。通过以下方式可以借助catch来更改这个默认行为: catch Expression 若表达式的求值过程没有发生错误,则catchExpression返回Expression的值。于是catchatom_to_list(abc)会返回[97,98,99]、catch22会返回22。若求值过程失败,catchExpression将返回元组{'EXIT',Reason},其中Reason是用于指明错误原因的原子式(参见第??节)。于是catchan_atom-2会返回{'EXIT',badarith}、catchatom_to_list(123)会返回{'EXIT',badarg}。函数执行结束后,控制流程便返还者。throw/1可以令控制流程跳过调用者。如果我们像上述的那样计算catchExpression,并在Expression的求值过程中调用throw/1,则控制流程将直接返回至catch。注意catch可以嵌套;在嵌套的情况下,一次失败或throw将返回至最近的catch处。在catch之外调用throw/1将导致运行时错误。下面的例子描述了catch和throw的行为。定义函数foo/1: foo(1) -> hello;foo(2) -> throw({myerror, abc});foo(3) -> tuple_to_list(a);foo(4) -> exit({myExit, 222}). 假设在不使用catch的情况下,一个进程标识为Pid的进程执行了这个函数,则:foo(1)
返回
hello。
foo(2)
执行
throw({myerror,abc})。由于不在catch的作用域内,执行foo(2)的进程将出错退出。
foo(3)
执行
foo(3)的进程执行BIFtuple_to_list(a)。这个BIF用于将元组转换为列表。在这个例子中,参数不是元组,因此该进程将出错退出。
foo(4)
执行BIF
exit/1。由于不在catch的范围内,执行foo(4)的函数将退出。很快我们就会看到参数{myExit,222}的用途。
foo(5)
执行
foo(5)的进程将出错退出,因为函数foo/1的首部无法匹配foo(5)。
现在让我们来看看在catch的作用域内对foo/1以相同的参数进行求值会发生什么:
demo(X) ->
case catch foo(X) of
{myerror, Args} ->
{user_error, Args};
{'EXIT', What} ->
{caught_error, What};
Other ->
Other
end.
``` `demo(1)`
> 像原来一样执行`hello`。因为没有任何失败发生,而我们也没有执行`throw`,所以`catch`直接返回`foo(1)`的求值结果。
`demo(2)`
> 求值结果为`{user_error,abc}`。对`throw({myerror,abc})`的求值导致外围的`catch`返回`{myerror, abc}`同时`case`语句返回`{user_error,abc}`。
`demo(3)`
> 求值结果为`{caught_error,badarg}`。`foo(3)`执行失败导致`catch`返回`{'EXIT',badarg}`。
`demo(4)`
> 求值结果为`{caught_error,{myexit,222}}`。
`demo(5)`
> 求值结果为`{caught_error,function_clause}`。
注意,在`catch`的作用域内,借助`{'EXIT',Message}`,你能够很容易地“伪造”一次失败——这是一个**设计决策**[[1]](/read/Concurrent-Programming-in-Erlang/d001#id16)。
### 使用catch和throw抵御不良代码
下面来看一个简单的Erlang shell脚本:
-module(s_shell). -export([go/0]).
go() -> eval(io:parse_exprs('=> ')), % '=>' is the prompt go().
eval({form,Exprs}) -> case catch eval:exprs(Exprs, []) of % Note the catch {'EXIT', What} -> io:format("Error: ~w!~n", [What]); {value, What, } -> io:format("Result: ~w~n", [What]) end; eval() -> io:format("Syntax Error!~n", []).
标准库函数`io:parse_exprs/1`读取并解析一个Erlang表达式,若表达式合法,则返回`{form,Exprs}`。
正确情况下,应该匹配到第一个子句`eval({form,Expr})`并调用库函数`eval:exprs/2`对表达式进行求值。由于无法得知表达式的求值过程是否为失败,我们在此使用`catch`进行保护。例如,对`1-a`进行求值将导致错误,但在`catch`内对`1-a`求值就可以捕捉这个错误[[2]](/read/Concurrent-Programming-in-Erlang/d001#id17)。借助`catch`,在求值失败时,`case`子句与模式`{'EXIT',what}`匹配,在求值成功时则会与`{value,What,_}`匹配。
### 使用catch和throw实现函数的非本地返回
假设我们要编写一个用于识别简单整数列表的解析器,可以编写如下的代码:
parse_list(['[',']' | T]) {nil, T}; parse_list(['[', X | T]) when integer(X) -> {Tail, T1} = parse_list_tail(T), {{cons, X, Tail}, T1}.
parse_list_tail([',', X | T]) when integer(X) -> {Tail, T1} = parse_list_tail(T), {{cons, X, Tail}, T1}; parse_list_tail([']' | T]) -> {nil, T}.
例如:
parse_list(['[',12,',',20,']']). {{cons,12,{cons,20,nil}},[]}
要是我们试图解析一个非法的列表,就会导致如下的错误:
try:parse_list(['[',12,',',a]). !!! Error in process <0.16.1> in function !!! try:parse_list_tail([',',a]) !!! reason function_clause ** exited: function_clause **
如果我们想在跳出递归调用的同时仍然掌握是哪里发生了错误,可以这样做:
parse_list1(['[',']' | T]) -> {nil, T}; parse_list1(['[', X | T]) when integer(X) -> {Tail, T1} = parse_list_tail1(T), {{cons, X, Tail}, T1}; parse_list1(X) -> throw({illegal_token, X}).
parse_list_tail1([',', X | T]) when integer(X) -> {Tail, T1} = parse_list_tail1(T), {{cons, X, Tail}, T1}; parse_list_tail1([']' | T]) -> {nil, T}; parse_list_tail1(X) -> throw({illegal_list_tail, X}).
现在,如果我们在`catch`里对`parse_list/1`求值,将获得以下结果:
catch parse_list1(['[',12,',',a]). {illegal_list_tail,[',',a]} ```
通过这种方式,我们得以从递归中直接退出,而不必沿着通常的递归调用路径逐步折回。
