正面:您不必担心递归调用的返回逻辑;既然你找到了你需要的东西,为什么还要担心如何将这个引用备份到方法堆栈中.
否定:您有一个不需要的堆栈跟踪. try / catch块也变得违反直觉.
这是一个愚蠢的简单用法:
public class ThrowableDriver { public static void main(String[] args) { ThrowableTester tt = new ThrowableTester(); try { tt.rec(); } catch (TestThrowable e) { System.out.print("All good\n"); } } } public class TestThrowable extends Throwable { } public class ThrowableTester { int i=0; void rec() throws TestThrowable { if(i == 10) throw new TestThrowable(); i++; rec(); } }
问题是,是否有更好的方法来达到同样的目的?另外,以这种方式做事有什么不妥之处吗?
解决方法
有一个经典的反对意见是“例外并不比一个goto更好”,这显然是错误的.在Java和大多数其他合理的现代语言中,您可以拥有嵌套的异常处理程序和最终的处理程序,因此当通过异常传输控件时,精心设计的程序可以执行清理等.事实上,这样的异常在几个方面更可取返回代码,因为使用返回代码,您必须在每个返回点添加逻辑以测试返回代码,并在退出例程之前查找并执行正确的finally逻辑(可能是几个嵌套的部分).使用异常处理程序,通过嵌套异常处理程序可以相当自动化.
例外确实带来了一些“包袱” – 例如Java中的堆栈跟踪.但Java异常实际上非常有效(至少与其他语言中的实现相比),因此如果不过度使用异常,性能应该不是一个大问题.
[我将补充一点,我有40年的编程经验,自70年代末以来我一直在使用异常.独立“发明”try / catch / finally(称为BEGIN / ABEXIT / EXIT)1980年.]
一个“非法”的题外话:
我认为在这些讨论中经常遗漏的事情是计算中的第一个问题不是成本或复杂性或标准或性能,而是控制.
通过“控制”,我不是指“控制流”或“控制语言”或“运算符控制”或经常使用术语“控制”的任何其他上下文.我的意思是“控制复杂性”,但它不仅仅是 – 它是“概念控制”.
我们都做到了(至少我们这些已经编程超过6周的人) – 开始编写一个没有真正结构或标准的“简单小程序”(除了我们可能习惯使用的那些),不要担心它的复杂性,因为它“简单”和“一次性”.但是,在10个案例或100个案例中,根据具体情况,“简单的小程序”可能会变成怪物.
我们放松了对它的“概念控制”.修复一个bug会引入另外两个bug.程序的控制和数据流变得不透明.它的表现方式我们无法理解.
然而,按照大多数标准,这个“简单的小程序”并不复杂.它并不是那么多行代码.很可能(因为我们是熟练的程序员),它被分解为“适当”数量的子程序.通过复杂性测量算法运行它并且可能(因为它仍然相对较小并且“子程序化”)它将得分并不特别复杂.
最终,维护概念控制是几乎所有软件工具和语言背后的驱动力.是的,像汇编程序和编译器这样的东西使我们更有效率,生产力是所宣称的驱动力,但是提高生产力的大部分原因是因为我们不必忙于“不相关”的细节,而是可以专注于我们想要的概念实施.
概念控制的重大进步发生在计算历史的早期,因为“外部子程序”的出现变得越来越独立于它们的环境,允许“关注点分离”,子程序开发人员不需要了解子程序的环境,并且子程序的用户不需要了解子程序内部.
BEGIN / END和“{…}”的简单开发产生了类似的进步,因为即使是“内联”代码也可以从“在那里”和“在这里”之间的某些隔离中受益.
我们认为理所当然的许多工具和语言功能都很有用,因为它们有助于保持对更复杂的软件结构的智能控制.人们可以通过它如何帮助实现这种智能控制,非常准确地衡量新工具或功能的效用.
其中一个问题是剩下的最大困难是资源管理.这里的“资源”是指任何实体 – 对象,打开文件,分配堆等 – 可能在程序执行过程中“创建”或“分配”,随后需要某种形式的释放. “自动堆栈”的发明是这里的第一步 – 变量可以“堆叠”分配,然后当“分配”它们的子程序退出时自动删除. (这是一个非常有争议的概念,许多“当局”建议不要使用该功能,因为它会影响性能.)
但是在大多数(全部?)语言中,这个问题仍然以某种形式存在.使用显式堆的语言需要“删除”任何“新”,例如.必须以某种方式关闭打开的文件.锁必须被释放.其中一些问题可以是精细的(使用GC堆,例如)或纸糊(引用计数或“父母”),但是没有办法消除或隐藏所有这些问题.而且,虽然在简单的情况下管理这个问题是相当简单的(例如,新的对象,调用使用它的子程序,然后删除它),现实生活很少那么简单.有一种方法可以进行十几个不同的调用,在调用之间有点随机分配资源,这些资源具有不同的“生命周期”,这种情况并不少见.并且一些调用可能返回改变控制流的结果,在某些情况下导致子例程退出,或者它们可能导致子例程体的某个子集周围的循环.知道如何在这种情况下释放资源(释放所有正确的资源而不是错误的资源)是一项挑战,随着子程序随着时间的推移而变得更加复杂(因为所有复杂的代码都是如此).
try / finally机制的基本概念(暂时忽略了catch方面)很好地解决了上述问题(尽管远非完美,但我承认).对于需要管理的每个新资源或资源组,程序员引入try / finally块,将释放逻辑放在finally子句中.除了确保资源将被释放的实际方面之外,这种方法的优点是可以清楚地描述所涉及资源的“范围”,提供一种“有力维护”的文档.
这种机制与catch机制相结合的事实有点意外,因为在正常情况下用于管理资源的相同机制用于在“异常”情况下管理它们.由于“异常”(表面上)是罕见的,因此最小化该稀有路径中的逻辑量总是明智的,因为它永远不会像主线那样经过良好测试,并且因为“概念化”错误情况对于平均值来说特别困难程序员.
当然,尝试/最后有一些问题.其中第一个是块可以嵌套得如此深,以至于程序结构变得模糊而不是澄清.但这是与do循环和if语句相同的问题,它等待语言设计者的一些启发性见解.更大的问题是,尝试/最终有抓住(甚至更糟的是,例外)行李,这意味着它不可避免地被降级为二等公民. (例如,除了现在已弃用的JSB / RET机制之外,最终甚至不存在Java字节码中的概念.)
还有其他方法. IBM iSeries(或“System i”或“IBM i”或他们现在称之为的任何内容)具有将清理处理程序附加到调用堆栈中的给定调用级别的概念,以便在关联程序返回(或异常退出)时执行).虽然这在当前形式下是笨拙的,并不真正适合Java程序中所需的精细控制级别,例如,它确实指向了潜在的方向.
当然,在C语言系列(但不是Java)中,能够将代表资源的类实例化为自动变量,并使对象析构函数在退出变量范围时提供“清理”. (注意,这个方案,在本质上,主要使用try / finally.)这在很多方面都是一种很好的方法,但它需要一套通用的“清理”类或者为每种不同类型定义一个新类.资源,创造一个文本庞大但相对无意义的类定义的潜在“云”. (而且,正如我所说,它不是Java目前形式的选择.)
但我离题了.