6

我总是在错误的上下文中看到 Throwable/Exception。但是我可以想到一些情况,扩展 aThrowable只是为了摆脱一堆递归方法调用会非常好。例如,假设您试图通过递归搜索的方式在树中查找并返回某个对象。一旦你发现它把它粘在某个地方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 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();
    }
}

问题是,有没有更好的方法来实现同样的目标?此外,以这种方式做事有什么本质上的坏处吗?

4

6 回答 6

8

实际上,在某些“普通”程序员不会想到使用异常的情况下使用异常是一个好主意。例如,在启动“规则”并发现它不起作用的解析器中,异常是一种很好的方式来回退到正确的恢复点。(这在一定程度上类似于您提出的脱离递归的建议。)

有一个经典的反对意见,即“异常并不比 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/finallycatch(甚至更糟的是例外)包袱,这意味着它不可避免地被降级为二等公民。(例如,finally除了现在已弃用的 JSB/RET 机制之外,它甚至不作为 Java 字节码中的概念存在。)

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

当然,在 C++ 语言家族(但不是 Java)中,可以将代表资源的类实例化为自动变量,并让对象析构函数在退出变量范围时提供“清理”。(请注意,这个方案实际上是在使用 try/finally。)这在很多方面都是一种出色的方法,但它需要一套通用的“清理”类或为每种不同类型定义一个新类资源,创建一个潜在的“云”文本庞大但相对无意义的类定义。(而且,正如我所说,它不是当前形式的 Java 的一种选择。)

但我离题了。

于 2011-08-01T03:51:05.807 回答
3

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

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

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

于 2011-08-01T03:26:07.717 回答
2

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

传统智慧:“例外是针对特殊情况的。” 正如您所注意到的,Throwable理论上听起来更笼统,但除了异常和错误之外,它似乎并不是为更广泛的用途而设计的。从文档

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

许多运行时 (VM) 旨在不围绕抛出异常进行优化,这意味着它们可能“昂贵”。当然,这并不意味着您不能这样做,而且“昂贵”是主观的,但通常不会这样做,其他人会在您的代码中发现它感到惊讶。

于 2011-08-01T03:27:14.670 回答
1

问题是,有没有更好的方法来实现同样的目标?此外,以这种方式做事有什么本质上的坏处吗?

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

此外,例外相当于受控的 goto,几乎相当于跳远。是的,是的,它们可以嵌套,在像 Java 这样的语言中,你可以拥有漂亮的“finally”块等等。尽管如此,它们仅此而已,因此,它们并不是典型控制结构的一般情况替代品。四十多年的集体工业知识告诉我们,一般来说,我们应该避免此类事情,除非您有非常正当的理由这样做。

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

// 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 good\n");
        }
    }
}

public class ThrowableTester {
    int i=0;

    void rec() {
        if(i == 10) return;
        i++;
        rec();
    }
}

看?更简单。更少的代码行。没有多余的 try/catch 或不必要的异常抛出。你达到同样的效果。

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

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

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

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

既然你找到了你需要的东西,为什么还要担心如何将该引用备份到方法堆栈中。

在方法完成之前将返回(几乎是 JVM 中的对象引用)推送到堆栈比完成所有与抛出异常有关的簿记(运行 Epilogs 和填充潜在的大堆栈跟踪)更便宜并捕获它(遍历堆栈跟踪。) JVM 与否,这是基本的 CS 101 内容。

因此,不仅它更昂贵,您还必须输入更多字符来编码相同的东西。

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

于 2011-08-01T16:57:38.583 回答
0

只是。不。
请参阅:Joshua Bloch 撰写的 Effective Java,第 10 页。243

于 2011-08-01T17:15:56.553 回答
-1

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

于 2021-04-04T21:01:28.630 回答