在我所知的所有异常感知语言中(C ++,Java,C#,Python,Delphi-Pascal,PHP),捕获异常需要显式try
块后跟catch
块。我经常想知道技术原因是什么。为什么我们不能将catch
子句附加到其他普通的代码块中?作为一个C ++示例,我们为什么要写这个:
int main()
{
int i = 0;
try {
i = foo();
}
catch (std::exception& e)
{
i = -1;
}
}
而不是:
int main()
{
int i = 0;
{
i = foo();
}
catch (std::exception& e)
{
i = -1;
}
}
是否存在实施原因,或者仅仅是#34;有人首先以这种方式设计它,现在每个人都熟悉并复制它?"
我看到它的方式,编译语言没有任何意义 - 编译器在生成任何代码之前会看到整个源代码树,因此它可以轻松地在一个块前面插入try
关键字当catch
子句跟在该块之后(如果它需要首先为try
块生成特殊代码)。我可以想象在解释语言中有一些用法,它们不需要提前解析,同时需要在try
块开始时采取一些操作,但我不知道是否存在任何此类语言。
让我们抛开语言,而没有明确的方式来声明任意块(例如Python)。在所有其他方面,是否有技术原因需要try
关键字(或等效的)?
答案 0 :(得分:8)
设计语言时的一般想法是尽可能早地指出您所构建的构造,以便编译器不必执行不必要的工作。你建议的是要记住每个{}
块作为一个可能的try
块开始,只是发现其中大部分都没有。你会发现Pascal,C,C ++,Java等中的每个语句都是由一个关键字引入的,唯一的例外是赋值语句。
答案 1 :(得分:2)
从实际角度讲:通过指定try
,您可以在catch
异常中实现更好的模块化。具体来说,它允许更简洁的嵌套异常处理。要添加到EJP的答案,它会在捕获块嵌入其他块时增加可读性。可读性 是一个重要的考虑因素,当有多个嵌套{}
块时,try
为离散catch
es添加了一个很好的参考点。
答案 2 :(得分:2)
这个问题有几种答案,所有这些都可能是相关的。
第一个问题是关于效率以及编译语言和解释语言之间的区别。基本的直觉是正确的,语法的细节不会影响生成的代码。解析器通常生成一个抽象语法树(明确或隐式),无论是编译器还是解释器。一旦AST到位,用于生成AST的语法细节就无关紧要了。
接下来的问题是,是否要求显式关键字有助于解析。简单的答案是,它没有必要,但可以提供帮助。要理解为什么没有必要,你必须知道"前瞻设置"是一个解析器。前瞻集是每个解析状态的一组令牌,如果它们出现在令牌流中的下一个,它们将是正确的语法。诸如bison
之类的解析器生成器明确地为此前瞻设置建模。递归下降解析器也有一个先行集,但它们通常不会在表中显式出现。
现在考虑一种语言,如问题中所提出的,使用以下语法进行例外处理:
block: "{" statement_list "}" ;
statement: block ;
statement: block "catch" block ;
statement: //... other kinds of statements
使用这种语法,可以使用异常块来装饰块。关于歧义的问题是,在看到block
之后,catch
关键字是否含糊不清。假设catch
关键字是唯一的,那么解析器将识别异常装饰的语句是完全明确的。
现在我说,必须为解析器提供明确的try
关键字。它有什么用?它约束了某些解析器状态的前瞻集。 try
之后设置的前瞻是单个令牌{
。匹配的闭括号后面的前瞻设置是单个关键字catch
。表驱动的解析器并不关心这一点,但它使得手写的递归下降解析器更容易编写。但更重要的是,它改进了解析器中的错误处理。如果第一个块中出现语法错误,则使用try
关键字意味着错误恢复可以查找catch
令牌作为重新建立已知解析器状态的fence帖子,可能正是因为它是先行集的唯一成员。
关于try
关键字的最后一个问题与语言设计有关。简单地说,在块前面有明确的关键字使代码更容易阅读。人类仍然需要用眼睛解析代码,即使他们没有使用计算机算法来完成它。减少正式语法中前瞻集的大小也减少了第一眼看到代码段可能意味着什么的可能性。这提高了代码的清晰度。
答案 3 :(得分:1)
要求附加到块末端的控制结构在这些块之前与指示符配对,避免在以下情况下出现混淆:
if (condition1)
do {
action1();
} while(condition2);
else
action2();
想象一下,而不是do statement; while(condition)
C使用了语法statement; until(!condition);
这是否会使事情变得更加清晰?
if (condition1)
{
action1();
} until(!condition2);
else
action2();
我认为以前的代码片段完全可读,而不需要在第一个if
上单独的复合语句(没有单独的复合语句表明在if
中给出了首次通过条件的循环,以及下面给出的重复条件,下面有一个特殊的零迭代处理程序。我认为第二个版本似乎不太清楚。可以通过将循环包含在复合语句中来澄清第二个,但这会有效地添加比do
更多的详细信息。
答案 4 :(得分:0)
我问了一个问题,暗示了对这个问题的回答。
Are try blocks necessary or even helpful for the "zero-cost" stack unwinding strategy?
显式try
块可以更有效地实现异常处理,尤其是在抛出异常时。实现异常的两种流行策略是“setjmp / longjmp”和“零成本”。
以C标准库中的函数命名的setjmp / longjmp策略在进入try块时保存上下文信息。该信息将大致“在此上下文中,此类型的异常跳转到此地址,此其他类型的异常跳转到该地址,其他异常类型冒泡上下文堆栈”。这允许抛出的异常快速找到匹配的catch,但是在运行时需要保存上下文,即使没有抛出异常也是如此。
在零成本策略中,try块没有固有成本,但找到抛出异常的catch块很慢。进入try
块时,编译器不会在运行时保存上下文信息,而是在编译时构建表,可用于在给定抛出异常的原点的情况下查找catch块。该表指定了指令范围和关联的catch块。实现这一目标的一种方法是使用可变范围大小和二进制搜索。
setjmp / longjmp策略需要try
个块来知道何时保存上下文。
零成本策略不依赖于try块。
由于这两种方法在效率方面存在权衡,因此将选择留给语言实现者并为setjmp / longjmp策略提供所需的显式try
块是有意义的。