对异常以外的事物使用Throwable



我总是在错误的上下文中看到Throwable/Exception。但我可以想到这样的情况:扩展Throwable只是为了从递归方法调用的堆栈中分离出来,这将是非常好的。例如,您试图通过递归搜索的方式在树中查找并返回某个对象。一旦找到它,就把它放在某个Carrier extends Throwable中并抛出,然后在调用递归方法的方法中捕获它。

肯定:您不必担心递归调用的返回逻辑;既然已经找到了所需的内容,为什么还要担心如何将该引用带回方法堆栈呢。

否定:您有一个不需要的堆栈跟踪。此外,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 goodn");
}
}
}
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处理程序,因此当通过异常传递控制时,设计良好的程序可以执行清理等。事实上,通过这种方式,异常在某些方面比返回代码更可取,因为对于返回代码,您必须在每个返回点添加逻辑来测试返回代码,并在退出例程之前找到并执行正确的finally逻辑(可能是几个嵌套部分)。对于异常处理程序,这是相当自动的,通过嵌套的异常处理程序。

例外情况确实伴随着一些";行李"——Java中的堆栈跟踪,例如。但是Java异常实际上是非常有效的(至少与其他一些语言中的实现相比),所以如果你不太频繁地使用异常,那么性能应该不会是一个大问题。

[我要补充一点,我有40年的编程经验,从70年代末开始我就一直在使用异常。1980年独立发明了try/catch/finally(称之为BEGIN/ABEXIT/EXIT)。]

一个";非法的";题外话:

我认为在这些讨论中经常忽略的一点是,计算中的首要问题不是成本、复杂性、标准或性能,而是控制。

通过";控制";我不是说";"控制流";或";控制语言";或";操作员控制";或术语";控制";是经常使用的。我确实有点刻薄;"复杂性控制";,但它不止于此——它是";概念控制";。

我们都做到了(至少我们中那些编程时间超过6周的人)——开始写一个";简单的小程序";没有真正的结构或标准(除了我们可能习惯使用的标准),不担心它的复杂性,因为它是";简单的";和一个";"一次性";。但是,根据上下文的不同,在可能十分之一或百分之一的情况下;简单的小程序";长成一个怪物。

我们松了";概念控制";修复一个错误会带来两个错误。程序的控制和数据流变得不透明。它的行为方式我们无法完全理解。

然而,按照大多数标准;简单的小程序";并没有那么复杂。实际上并没有那么多行代码。很可能(因为我们是熟练的程序员)它被分解为";适当的";子程序的数量。通过复杂性测量算法运行它,很可能(因为它仍然相对较小并且"子程序化")它会被认为不是特别复杂。

最终,保持概念控制是几乎所有软件工具和语言背后的驱动力。是的,像汇编程序和编译器这样的东西让我们更有生产力,生产力是声称的驱动力,但生产力的提高很大程度上是因为我们不必忙于";"无关";详细信息,可以专注于我们想要实现的概念。

概念控制的主要进步出现在计算历史的早期;外部子程序";并变得越来越独立于他们的环境;关注点分离";其中子例程开发人员不需要对子例程的环境了解太多,而子例程的用户不需要对子程序内部了解太多。

BEGIN/END和";{…}";产生了类似的进步;内联";代码可以受益于";在那里";以及";在这里";。

许多我们认为理所当然的工具和语言功能都存在,而且很有用,因为它们有助于保持对越来越复杂的软件结构的智能控制。人们可以通过一个新工具或功能如何帮助这种智能控制来非常准确地衡量它的效用。

如果说剩下的最大困难领域是资源管理的话。通过";资源";这里,我指的是任何实体——对象、打开的文件、分配的堆等——可能是";创建";或";分配的";在程序执行过程中,并且随后需要某种形式的释放。";"自动堆叠";是这里的第一步——变量可以被分配";在堆栈上";然后当子程序";分配的";他们离开了。(这曾经是一个非常有争议的概念,许多"权威机构"建议不要使用该功能,因为它会影响性能。)

但在大多数(所有?)语言中,这个问题仍然以某种形式存在。使用显式堆的语言需要";删除";不管你说什么;新的";,打开的文件必须以某种方式关闭。锁必须松开。其中一些问题可以被处理(例如,使用GC堆)或掩盖(引用计数或"养育"),但没有办法消除或隐藏所有这些问题。而且,虽然在简单的情况下管理这个问题是相当直接的(例如,new是一个对象,调用使用它的子例程,然后是delete),但现实生活很少这么简单。有一种方法可以进行十几个不同的调用,在调用之间随机分配资源,并使用不同的";寿命";这些资源。一些调用可能会返回更改控制流的结果,在某些情况下会导致子例程退出,或者它们可能会导致围绕子例程主体的某个子集的循环。知道如何在这种情况下释放资源(释放所有正确的资源,不释放任何错误的资源)是一个挑战,而且随着时间的推移,子例程的修改会变得更加复杂(就像所有复杂的代码一样)。

try/finally机制的基本概念(暂时忽略catch方面)很好地解决了上述问题(尽管我承认远非完美)。对于每一个需要管理的新资源或资源组,程序员都会引入一个try/finally块,将释放逻辑放在finally子句中。除了确保资源将被释放的实际方面之外,这种方法的优点是清楚地描绘了";"范围";在所涉及的资源中;强制维护";。

该机制与catch机制耦合的事实有点意外,因为在正常情况下用于管理资源的相同机制在";异常";案例由于";例外情况";是(表面上)罕见的,明智的做法总是尽量减少这种罕见路径中的逻辑量,因为它永远不会像主线那样经过良好的测试;概念化";错误案例对于普通程序员来说尤其困难。

诚然,try/finally存在一些问题。其中的第一个问题是,这些块可能嵌套得太深,以至于程序结构变得模糊而不是清晰。但这是do循环和if语句的共同问题,需要等待语言设计者的启发。更大的问题是,try/finallycatch(甚至更糟的是,例外)的包袱,这意味着它不可避免地被降级为二等公民。(例如,finally在Java字节码中甚至不作为一个概念存在,除了现在被弃用的JSB/RET机制之外。)

还有其他方法。IBM iSeries(或"System i"或"IBM i",或他们现在所称的任何东西)具有将清理处理程序附加到调用堆栈中给定调用级别的概念,以便在相关程序返回(或异常退出)时执行。虽然在目前的形式下,这是笨拙的,并不真正适合Java程序中所需的精细控制级别,例如,它确实指向了潜在的方向。

当然,在C++语言家族(但不是Java)中,有能力将代表资源的类实例化为自动变量,并使对象析构函数提供";清除";从变量的作用域退出时。(请注意,这个方案在本质上是使用try/finaly。)这在很多方面都是一个很好的方法,但它需要一套通用的";清除";类或为每种不同类型的资源定义一个新类;云;文本庞大但相对无意义的类定义。(而且,正如我所说,对于目前形式的Java来说,这不是一个选项。)

但我离题了。

对程序控制流使用异常不是一个好主意。

为超出正常操作标准的情况保留例外情况。

关于SO:有相当多的相关问题

  • "使用异常控制流"示例

  • Java异常的速度有多慢?

  • 为什么不使用异常作为常规控制流?

语法变得不稳定,因为它们不是为通用控制流设计的。递归函数设计中的标准做法是返回一个sentinel值或找到的值(或者什么都不返回,这在您的示例中会起作用)。

传统观点认为:"例外情况适用于特殊情况。"正如你所注意到的,Throwable在理论上听起来更为笼统,但除了例外和错误之外,它似乎并不是为更广泛的使用而设计的。来自文档:

Throwable类是所有错误和异常的超类在Java语言中。

许多运行时(VM)的设计并不是为了围绕抛出异常进行优化,这意味着它们可能会"昂贵"。当然,这并不意味着你不能做到这一点,"昂贵"是主观的,但通常情况下,这是不可能的,其他人会惊讶地在你的代码中发现它。

问题是,有更好的方法来实现同样的目标吗?而且以这种方式做事是不是有本质上的不好?

关于第二个问题,无论编译器的效率如何,异常都会带来巨大的运行时负担。仅凭这一点就不应该在一般情况下将它们用作控制结构

此外,异常相当于受控的goto,几乎相当于长跳。是的,是的,它们可以嵌套,在像Java这样的语言中,你可以拥有漂亮的"finally"块等等。尽管如此,这就是它们的全部,因此,它们并不意味着是典型控制结构的一般情况替代品。40多年的集体工业知识告诉我们,一般来说,我们应该避免这样的事情除非你有非常合理的理由这样做

这就是你第一个问题的核心。是的,有一种更好的方法(以您的代码为例)。。。只需使用典型的控制结构:

// class and method names remain the same, though using 
// your typical logical control structures
public class ThrowableDriver {
public static void main(String[] args) {
ThrowableTester tt = new ThrowableTester();
tt.rec();
System.out.print("All goodn");
}
}
}
public class ThrowableTester {
int i=0;
void rec() {
if(i == 10) return;
i++;
rec();
}
}

看到了吗?更简单。代码行数减少。没有多余的try/catch或不必要的异常抛出。你也做到了。

最后,我们的工作不是处理语言结构,而是创建合理的程序,从可维护性的角度来看,这些程序足够简单,只有足够的语句来完成任务,而不需要其他任何东西。

因此,当谈到您提供的示例代码时,您必须问问自己:使用这种方法,我得到了什么,而在使用典型的控制结构时,我无法得到这些?

您不必担心递归调用的返回逻辑;

如果您不担心返回逻辑,那么只需忽略返回或将方法定义为void类型。将其封装在try/catch中只会使代码变得更加复杂。如果你不关心回报,我相信你关心的是完成的方法。因此,您只需要简单地调用它(就像我在本文中提供的代码示例中一样)。

既然你找到了你需要的,为什么要担心如何将该引用带回方法堆栈呢。

在方法完成之前将返回(相当于JVM中的对象引用)推送到堆栈比抛出异常(运行epilogs并填充潜在的大堆栈跟踪)并捕获它(遍历堆栈跟踪)所涉及的所有簿记都要便宜。不管JVM与否,这都是CS101的基本内容。

因此,它不仅更昂贵,而且还必须键入更多的字符才能编写相同的代码。

实际上没有递归方法可以通过Throwable退出,而Throwable不能使用典型的控制结构重写。您需要有一个非常、非常但非常好的理由来使用异常来代替控制结构。

只是。不要
参见:约书亚·布洛赫的《高效Java》,第243页

我不知道这是否是个好主意,但是,在设计CLI(不使用准备好的库)时,我突然想到,在不扰乱系统堆栈的情况下,处理从应用程序中的某个位置返回的一种自然方法是使用Throwable(如果您只是调用使用该方法的方法,如果有人说在应用程序菜单中向前和向后移动了大约255次,则会得到stack OVER FLOW)。由于返回使用Throwable与您在应用程序中的位置无关,它使我能够使方法抽象(从字面意义上讲),即,由类X的一些条目组成的所有菜单都用一个方法处理。

最新更新