实际上,在某些“普通”程序员不会想到使用异常的情况下使用异常是一个好主意。例如,在启动“规则”并发现它不起作用的解析器中,异常是一种很好的方式来回退到正确的恢复点。(这在一定程度上类似于您提出的脱离递归的建议。)
有一个经典的反对意见,即“异常并不比 goto 更好”,这显然是错误的。在 Java 和大多数其他相当现代的语言中,您可以拥有嵌套的异常处理程序和finally
处理程序,因此当通过异常传输控制权时,设计良好的程序可以执行清理等。事实上,通过这种方式,异常在几个方面优于返回代码,因为使用返回代码,您必须在每个返回点添加逻辑以测试返回代码并finally
在退出例程之前找到并执行正确的逻辑(可能是几个嵌套的部分)。对于异常处理程序,这是相当自动化的,通过嵌套的异常处理程序。
异常确实伴随着一些“包袱”——Java 中的堆栈跟踪,例如。但是 Java 异常实际上非常有效(至少与其他一些语言的实现相比),因此如果您没有过多地使用异常,性能应该不是一个大问题。
[我要补充一点,我有 40 年的编程经验,自 70 年代后期以来我一直在使用异常。大约在 1980 年独立地“发明”了 try/catch/finally(称为 BEGIN/ABEXIT/EXIT)。]
一个“非法”的题外话:
我认为在这些讨论中经常遗漏的事情是计算中的第一大问题不是成本或复杂性或标准或性能,而是控制。
我所说的“控制”并不是指“控制流”或“控制语言”或“操作员控制”或经常使用“控制”一词的任何其他上下文。我的意思是“控制复杂性”,但不止于此——它是“概念控制”。
我们都做到了(至少我们这些编程时间超过 6 周的人)——开始编写一个没有真正结构或标准的“简单的小程序”(除了我们可能习惯使用的那些),不用担心它的复杂性,因为它是“简单的”和“一次性的”。但是,在 10 分之一或 100 分之一的情况下,根据上下文,“简单的小程序”会变成怪物。
我们对它失去了“概念控制”。修复一个错误会引入另外两个错误。程序的控制和数据流变得不透明。它的行为方式是我们无法完全理解的。
然而,按照大多数标准,这个“简单的小程序”并没有那么复杂。代码行数并不多。很可能(因为我们是熟练的程序员)它被分解成“适当”数量的子例程。通过复杂性测量算法运行它,并且可能(因为它仍然相对较小并且“子程序化”)它的得分不是特别复杂。
最终,保持概念控制是几乎所有软件工具和语言背后的驱动力。是的,汇编器和编译器之类的东西让我们更有生产力,而生产力是所谓的驱动力,但生产力的提高很大程度上是因为我们不必忙于“不相关”的细节,而是可以专注于我们想要的概念来实施。
概念控制的重大进步发生在计算历史的早期,因为“外部子程序”出现并且越来越独立于它们的环境,允许“关注点分离”,其中子程序开发人员不需要对子程序的环境了解太多,并且子程序的用户不需要对子程序内部了解太多。
BEGIN/END 和“{...}”的简单开发产生了类似的进步,因为即使是“内联”代码也可以从“out there”和“in here”之间的某种隔离中受益。
我们认为理所当然的许多工具和语言特性都存在并且很有用,因为它们有助于保持对越来越复杂的软件结构的智能控制。人们可以通过它如何帮助这种智能控制来非常准确地评估新工具或功能的效用。
如果剩下的最大困难领域之一是资源管理。这里的“资源”是指任何实体——对象、打开的文件、分配的堆等——它们可能在程序执行过程中被“创建”或“分配”,随后需要某种形式的解除分配。“自动堆栈”的发明是第一步——变量可以“在堆栈上”分配,然后在“分配”它们的子程序退出时自动删除。(这曾经是一个非常有争议的概念,许多“权威”建议不要使用该功能,因为它会影响性能。)
但是在大多数(所有?)语言中,这个问题仍然以一种或另一种形式存在。使用显式堆的语言需要“删除”任何你“新”的东西,例如。打开的文件必须以某种方式关闭。必须释放锁。其中一些问题可以被巧妙地解决(例如,使用 GC 堆)或掩盖(引用计数或“父级”),但没有办法消除或隐藏所有这些问题。而且,虽然在简单情况下管理这个问题是相当直接的(例如,new
一个对象,调用使用它的子程序,然后delete
它),现实生活很少那么简单。有一种方法可以进行十几个不同的调用,在调用之间随机分配资源,这些资源具有不同的“生命周期”,这种情况并不少见。并且某些调用可能会返回改变控制流的结果,在某些情况下会导致子例程退出,或者它们可能会导致围绕子例程主体的某个子集的循环。知道如何在这种情况下释放资源(释放所有正确的资源而不释放错误的资源)是一个挑战,并且随着时间的推移修改子例程(就像所有复杂的代码一样),它变得更加复杂。
机制的基本概念try/finally
(暂时忽略了catch
方面)很好地解决了上述问题(尽管我承认远非完美)。对于需要管理的每个新资源或资源组,程序员都会引入一个try/finally
块,将释放逻辑放在 finally 子句中。除了确保资源将被释放的实际方面,这种方法的优点是清楚地描述了所涉及资源的“范围”,提供了一种“强制维护”的文档。
这种机制与机制相结合的事实catch
有点意外,因为在正常情况下用于管理资源的相同机制在“异常”情况下也用于管理它们。由于“异常”(表面上)很少见,因此尽量减少该罕见路径中的逻辑量总是明智的,因为它永远不会像主线那样经过良好的测试,而且因为“概念化”错误案例对于一般人来说特别困难程序员。
诚然,try/finally
有一些问题。其中第一个是块可以嵌套得太深,以至于程序结构变得模糊而不是清晰。但这是do
循环和if
语句的共同问题,它等待语言设计者的一些启发性见解。更大的问题是它try/finally
有catch
(甚至更糟的是例外)包袱,这意味着它不可避免地被降级为二等公民。(例如,finally
除了现在已弃用的 JSB/RET 机制之外,它甚至不作为 Java 字节码中的概念存在。)
还有其他方法。IBM iSeries(或“System i”或“IBM i”或他们现在所称的任何名称)具有将清理处理程序附加到调用堆栈中给定调用级别的概念,以便在相关程序返回(或异常退出时执行) )。虽然这在目前的形式下是笨拙的,并不真正适合 Java 程序所需的精细控制,例如,它确实指向了一个潜在的方向。
当然,在 C++ 语言家族(但不是 Java)中,可以将代表资源的类实例化为自动变量,并让对象析构函数在退出变量范围时提供“清理”。(请注意,这个方案实际上是在使用 try/finally。)这在很多方面都是一种出色的方法,但它需要一套通用的“清理”类或为每种不同类型定义一个新类资源,创建一个潜在的“云”文本庞大但相对无意义的类定义。(而且,正如我所说,它不是当前形式的 Java 的一种选择。)
但我离题了。