此 EEP 提议扩展 try/catch
语句,允许检索调用堆栈回溯 (堆栈跟踪),而无需调用 erlang:get_stacktrace/0
。
我们将引入新的语法来检索调用堆栈回溯(以下称为堆栈跟踪)。目前,可以随时调用 erlang:get_stacktrace/0
来检索当前进程中发生的最后一个异常的堆栈跟踪。
try/catch
的当前语法是
try
Exprs
catch
[Class1:]ExceptionPattern1 [when ExceptionGuardSeq1] ->
ExceptionBody1;
[ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
我们建议以下异常子句头语法的扩展
Class:ExceptionPattern:Stacktrace [when ExceptionGuardSeq] ->
Stacktrace
必须是一个变量名,而不是一个模式。此外,Stacktrace
必须是未绑定的,并且不能在 ExceptionGuardSeq
中被引用。
这是一个例子
try
Exprs
catch
something_was_thrown ->
%% The default class is 'throw'.
.
.
.
throw:something_else_was_thrown ->
.
.
.
throw:thrown_with_interesting_stacktrace:Stk ->
%% The class 'throw' must be explicitly given when
%% the stacktrace is to be retrieved.
.
.
.
error:undef ->
%% Handle an undefined function specially.
.
.
.
C:E:Stk ->
%% Log any other exception and rethrow it.
log_exception(C, E, Stk),
raise(C, E, Stk)
end.
此功能的主要动机是能够弃用(并最终删除)erlang:get_stacktrace/0
。
erlang:get_stacktrace/0
的问题在于,它强制保留进程中最新异常的堆栈跟踪,直到发生另一个异常或进程终止。堆栈跟踪通常包括最后一个函数调用、BIF 调用或(在 OTP 21 中)失败的操作符的参数。参数的大小可以是任意的。
这是一个例子
1> catch abs(lists:seq(1, 1000)).
{'EXIT',{badarg,
[{erlang,abs,
[[1,2,3,4,5,6,7,8,9,10,11,12,13,
14,15,16,17,18,19,20|...]],
[]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
{erl_eval,expr,5,[{file,"erl_eval.erl"},{line,431}]},
{shell,exprs,7,[{file,"shell.erl"},{line,687}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]}}
2>
包含从 1 到 1000 的整数的列表将保留在导致异常的进程中,直到同一进程中发生另一个异常。
在未来的版本中,如果 erlang:get_stacktrace/0
已被更改为始终返回 []
或已删除,则不再需要将堆栈跟踪无限期地保留在进程中。
另一个动机是,可以避免以下示例中的陷阱
try
Expr
catch
C:E ->
do_something(),
log_exception(C, E, erlang:get_stacktrace())
end
如果 do_something()
生成并捕获异常,则对 erlang:get_stacktrace/0
的调用将检索错误的堆栈跟踪。
关于语法,我们考虑过在堆栈跟踪变量之前使用另一个标记而不是冒号。这将继续允许在检索堆栈跟踪时使类 throw
隐式化。也就是说,您可以编写一个异常模式,后跟一些特殊标记,后跟堆栈跟踪变量的名称,并且将隐式理解类 throw
。
我们拒绝了这一点,原因有两个
我们找不到合适的分割标记。我们最好的建议是 @
,但这行不通,因为 @
在原子中是允许的。像 /
这样的标记可能会混淆至少解析器(也可能是人类读者),因为模式允许包含常量表达式。双冒号 (::
) 不会引起任何歧义问题,但每个人都立即将其与类型声明联系起来。
实际上,当捕获类 throw
的异常时,几乎从不对堆栈跟踪感兴趣。
Stacktrace
必须是一个变量,而不是一个模式。原因有两个
一般来说,不鼓励对堆栈跟踪进行模式匹配。其目的是让人类检查它,以帮助调试。
允许对堆栈跟踪进行模式匹配会很昂贵。当发生异常时,会保存原始堆栈跟踪。原始堆栈跟踪包含从堆栈收集的有限数量的延续指针(默认情况下为 8)以及可能失败的函数调用或 BIF 调用的参数。将原始堆栈跟踪转换为可以匹配或显示的符号形式非常昂贵;通过仅允许一个变量,只有在匹配了子句并且即将执行其主体时才会进行转换。
在 OTP 20 中,我们在 erlang:get_stacktrace/0
的文档中引入了一个新的警告
只有在(直接或间接)从 try 表达式的作用域内调用
erlang:get_stacktrace/0
时,才能保证它返回堆栈跟踪。
我们的意图是通过限制作用域,可以在退出作用域时清除堆栈跟踪。例如,以下代码将继续工作,但堆栈跟踪将在离开 try/catch
时清除
try Expr
catch
C:R ->
{C,R,helper()}
end
helper() ->
erlang:get_stacktrace().
不幸的是,以下略有不同的示例会迫使我们做出艰难的选择
try Expr
catch
C:R ->
helper(C, R)
end
helper(C, R) ->
{C,R,erlang:get_stacktrace()}.
对 helper/2
的调用是尾递归的。如果要保持调用尾递归,则无法清除堆栈跟踪。相反,如果要清除堆栈跟踪,则该调用不能再是尾递归的。
另一个问题是,编译器无法警告所有不返回堆栈跟踪的 erlang:get_stacktrace/0
调用实例。它所能做的只是警告明显不起作用的调用,例如以下示例
try Expr
catch
C:R ->
.
.
.
end,
Stk = erlang:get_stacktrace(),
.
.
.
也就是说,只有在同一个函数中使用了 try/catch
或 catch
,并且紧接着调用了 erlang:get_stacktrace/0
,编译器才会发出警告。
我们可以将 erlang:get_stacktrace/0
的有用作用域限制为 try/catch
中子句的语法作用域。例如
try
Expr
catch
C:E ->
Stk = erlang:get_stacktrace(),
log_exception(C, E, Stk)
end
与引入新语法相比,这种解决方案似乎没有任何优势。开发人员仍然必须更新其程序(从辅助函数中删除对 erlang:get_stacktrace/0
的调用,并将其移动到 try/catch
的语法作用域中)。
由于新语法会在 OTP 20 及之前的版本中导致编译错误,因此不会影响任何现有的源代码。
抽象格式已经包含一个用于异常子句中堆栈跟踪的变量(名称为 _
)。这意味着许多操作抽象 Erlang 代码的工具将继续工作而无需任何更改。
可以分几个阶段弃用 erlang:get_stacktrace/0
BIF,以尽量减少更改的影响。
例如
在 OTP 21 中,编译器将发出警告,提示 erlang:get_stacktrace/0
已弃用。
在 OTP 23(或可能是 OTP 22)中,erlang:get_stacktrace/0
将开始返回 []
。许多尚未更新的程序将继续工作,只是如果引发异常,将无法使用堆栈跟踪来帮助调试。
在未来的某个版本(OTP 24?),可以删除 erlang:get_stacktrace/0
。
可以在 PR #1634 中找到实现。
本文档已置于公共领域。