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

Why not use exceptions as regular flow of control?

提问人:Peter 提问时间:4/8/2009 最后编辑:Peter 更新时间:9/25/2023 访问量:62802

问:

为了避免我本来可以在谷歌上搜索的所有标准答案,我将提供一个你们都可以随意攻击的例子。

C# 和 Java(以及许多其他类型)有很多类型,我根本不喜欢一些“溢出”行为(例如:)。type.MaxValue + type.SmallestValue == type.MinValueint.MaxValue + 1 == int.MinValue

但是,看到我的恶毒本性,我会通过将这种行为扩展到,比方说被覆盖的类型来增加一些侮辱。(我知道是密封在 .NET 中的,但为了这个例子,我使用的是与 C# 完全一样的伪语言,除了 DateTime 没有被密封)。DateTimeDateTime

重写的方法:Add

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

当然,如果可以很容易地解决这个问题,但事实仍然是,我只是不明白为什么你不能使用异常(从逻辑上讲,我可以看到,当性能是一个问题时,在某些情况下应该避免异常)。

我认为在许多情况下,它们比 if 结构更清晰,并且不会破坏该方法正在制定的任何合同。

恕我直言,“永远不要将它们用于常规程序流”的反应似乎并没有那么好,因为这种反应的强度可以证明是合理的。

还是我弄错了?

我读过其他帖子,处理各种特殊情况,但我的观点是,如果你们两者都是,那就没有错:

  1. 清楚
  2. 履行您的方法的合同

向我开枪。

异常 与语言无关

评论

3赞 4/8/2009
+1 我也有同感。除了性能之外,避免控制流异常的唯一充分理由是调用方代码的可读性更高,返回值。
4赞 kender 4/8/2009
是:如果发生了某些事情,则返回 -1,如果发生其他事情,则返回 -2,依此类推......真的比例外更具可读性吗?
2赞 Ingo 4/8/2009
可悲的是,一个人因为说真话而受到负面声誉:你的例子不可能用 if 语句来写。(这并不是说它是正确/完整的。
8赞 Stefan Haberl 4/17/2013
我认为,抛出异常有时可能是你唯一的选择。例如,我有一个业务组件,它通过查询数据库在其构造函数中初始化其内部状态。有时,数据库中没有适当的数据可用。在构造函数中引发异常是有效取消对象构造的唯一方法。这在类的契约(在我的情况下是 Javadoc)中明确说明,因此我没有问题,客户端代码在创建组件时可以(并且应该)捕获该异常并从那里继续。
1赞 Konrad Rudolph 1/27/2015
既然你提出了一个假设,你就有责任引用确凿的证据/理由。对于初学者来说,请说出您的代码优于更短的自文档语句的一个原因。你会发现这很难。换句话说:你的前提本身是有缺陷的,因此你从中得出的结论是错误的。if

答:

180赞 Anton Gogolev 4/8/2009 #1

例外基本上是非本地语句,具有后者的所有后果。使用异常进行流控制违反了最小惊讶原则,使程序难以阅读(请记住,程序首先是为程序员编写的)。goto

此外,这不是编译器供应商所期望的。他们希望很少抛出异常,而且他们通常让代码效率低下。引发异常是 .NET 中最昂贵的操作之一。throw

但是,某些语言(尤其是 Python)使用异常作为流控制构造。例如,如果没有其他项,迭代器将引发异常。甚至标准语言结构(例如)也依赖于此。StopIterationfor

评论

13赞 hasen 4/8/2009
嘿,例外并不令人惊讶!当你说“这是一个坏主意”,然后继续说“但这在 python 中是个好主意”时,你有点自相矛盾。
7赞 Peter 4/12/2009
我仍然完全不相信:1)效率是除了问题之外,许多非巴赫特程序都不在乎(例如用户界面)2)令人惊讶:就像我说的,这只是令人惊讶的,因为它没有被使用,但问题仍然存在:为什么不首先使用id?但是,既然这就是答案
6赞 Mark E. Haase 7/3/2013
+1 实际上,我很高兴您指出了 Python 和 C# 之间的区别。我不认为这是矛盾的。Python 更加动态,并且以这种方式使用异常的期望被融入到语言中。它也是 Python EAFP 文化的一部分。我不知道哪种方法在概念上更纯粹或更自洽,但我确实喜欢编写代码的想法,即执行其他人期望它做的事情,这意味着不同语言的不同风格。
17赞 Lukas Eder 1/11/2015
当然,与 不同的是,异常会正确地与调用堆栈和词法范围进行交互,并且不会使堆栈或作用域一团糟。goto
5赞 Marcin 7/17/2015
实际上,大多数 VM 供应商都期望异常,并有效地处理它们。正如@LukasEder所指出的,例外与goto完全不同,因为它们是结构化的。
39赞 cwap 4/8/2009 #2

我的经验法则是:

  • 如果可以执行任何操作来从错误中恢复,请捕获异常
  • 如果错误非常常见(例如,用户尝试使用错误的密码登录),请使用 returnvalues
  • 如果您无法执行任何操作来从错误中恢复,请将其保留为未捕获(或在主捕获器中捕获它以对应用程序进行一些半优雅的关闭)

我看到的异常问题是从纯粹的语法角度来看的(我很确定性能开销是最小的)。我不喜欢到处都是尝试块。

举个例子:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

..另一个示例可能是,当您需要使用工厂将某些内容分配给句柄时,该工厂可能会引发异常:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

因此,就我个人而言,我认为您应该保留罕见错误情况(内存不足等)的异常,并使用返回值(值类、结构或枚举)来执行错误检查。

希望我理解你的问题正确:)

评论

4赞 Blorgbeard 4/8/2009
回复:您的第二个示例 - 为什么不在 Build 之后将对 BestMethodEver 的调用放在 try 块中?如果 Build() 抛出异常,它将不会被执行,编译器很高兴。
3赞 cwap 4/8/2009
是的,这可能是你最终会得到的,但考虑一个更复杂的例子,其中 myInstance 类型本身可能会引发异常。方法范围内的其他内容也可以。你最终会得到很多嵌套的 try/catch 块:(
0赞 jasonnerothin 4/8/2009
您应该在 catch 块中执行 Exception 转换(转换为适合抽象级别的 Exception 类型)。仅供参考:“Multi-catch”应该进入 Java 7。
0赞 RobH 4/9/2009
仅供参考:在 C++ 中,您可以在尝试捕获不同的异常后放置多个捕获。
2赞 David Thornley 4/9/2009
对于收缩包装软件,您需要捕获所有异常。至少设置一个对话框,解释程序需要关闭,这里有一些难以理解的东西,你可以在错误报告中发送。
207赞 Brann 4/8/2009 #3

你有没有试过调试一个程序,在正常操作过程中每秒引发五个异常?

我有。

该程序非常复杂(它是一个分布式计算服务器),在程序的一侧稍作修改就很容易在完全不同的地方破坏某些东西。

我希望我能启动程序并等待异常发生,但在正常操作过程中,启动期间大约有 200 个异常

我的观点:如果你在正常情况下使用异常,你如何定位异常(即异常)情况?

当然,还有其他充分的理由不要过多地使用异常,尤其是在性能方面

评论

17赞 Brann 4/8/2009
示例:当我调试一个 .net 程序时,我从 Visual Studio 启动它,并要求 VS 在所有异常时中断。如果您依赖异常作为预期行为,我就不能再这样做了(因为它会中断 5 次/秒),并且找到代码中有问题的部分要复杂得多。
21赞 Grant Wagner 4/9/2009
+1 表示指出您不想创建一个异常大海捞针,在其中找到实际异常针。
18赞 Peter 4/12/2009
根本不明白这个答案,我觉得这里的人误解了,跟调试完全没有关系,而是和设计有关系。恐怕这是纯粹的循环推理。你的观点真的超出了前面所说的问题
20赞 Brann 4/12/2009
@Peter :D在不破坏异常的情况下进行窃听是很困难的,如果设计有很多异常,那么捕获所有异常是很痛苦的。我认为使调试变得困难的设计几乎部分被破坏了(换句话说,设计与调试有关,IMO)
9赞 Ken 12/11/2009
即使忽略我想调试的大多数情况与抛出的异常不对应的事实,您的问题的答案是:“按类型”,例如,我会告诉我的调试器只捕获 AssertionError 或 StandardError 或与发生的坏事相对应的东西。如果你在这方面有问题,那么你如何进行日志记录 - 你不是按级别和类记录,这样你就可以过滤它们吗?你也认为这是个坏主意吗?
8赞 paweloque 4/8/2009 #4

我认为您可以使用 Exceptions 进行流量控制。然而,这种技术也有另一面。创建异常是一件代价高昂的事情,因为它们必须创建堆栈跟踪。因此,如果您想更频繁地使用 Exceptions,而不仅仅是为了发出异常情况的信号,则必须确保构建堆栈跟踪不会对性能产生负面影响。

降低创建异常成本的最佳方法是重写 fillInStackTrace() 方法,如下所示:

public Throwable fillInStackTrace() { return this; }

此类异常不会填充堆栈跟踪。

评论

0赞 jasonnerothin 4/8/2009
堆栈跟踪还要求调用方“知道”(即依赖)堆栈中的所有可抛出对象。这是一件坏事。抛出适合抽象级别的异常(服务中的 ServiceExceptions、Dao 方法中的 DaoExceptions 等)。如有必要,只需翻译即可。
0赞 aef 6/2/2022
除了用 和 处理普通的异常之外,编程语言 Ruby 还有第二个特性,它允许你 和 符号。我认为这自然避免了创建堆栈跟踪的影响,而只是将一小段文本扔给堆栈上的下一个捕获器。raiserescuethrowcatch
1赞 Ingo 4/8/2009 #5

我觉得你的例子没有错。相反,忽略被调用函数抛出的异常将是一种罪过。

在 JVM 中,抛出异常的成本并不高,只需使用 new xyzException(...) 创建异常,因为后者涉及堆栈遍历。因此,如果您提前创建了一些异常,则可以多次抛出它们,而无需支付任何费用。当然,这样你就不能在异常的情况下传递数据,但我认为无论如何这是一件坏事。

评论

0赞 Ingo 4/8/2009
对不起,这完全是错误的,布兰。这取决于条件。情况并非总是微不足道。因此,if 语句可能需要数小时、数天甚至更长时间。
0赞 Ingo 4/8/2009
在 JVM 中,就是这样。不比退货贵。去想想。但问题是,如果不是被调用函数中已经存在的代码,你会在 if 语句中写什么,以区分异常情况和正常情况---从而代码重复。
1赞 Brann 4/8/2009
Ingo :特殊情况是您意想不到的。即一个你没有想到的程序员。所以我的规则是“编写不引发异常的代码”:)
1赞 Brann 4/8/2009
我从不编写异常处理程序,我总是解决问题(除非我不能这样做,因为我无法控制错误代码)。而且我从不抛出异常,除非我编写的代码是供其他人使用的(例如库)。不给我看矛盾吗?
1赞 Ingo 4/8/2009
我同意你的看法,不要疯狂地抛出例外。但可以肯定的是,什么是“例外”是一个定义问题。例如,如果 String.parseDouble 无法提供有用的结果,则会引发异常。它还应该做什么?返回 NaN?非IEEE硬件呢?
14赞 mouviciel 4/8/2009 #6

标准的 anwser 是例外不是常规的,应该在特殊情况下使用。

一个对我来说很重要的原因是,当我在我维护或调试的软件中读取控制结构时,我试图找出为什么原始编码人员使用异常处理而不是结构。我希望能找到一个好的答案。try-catchif-else

请记住,您不仅要为计算机编写代码,还要为其他编码人员编写代码。有一个与异常处理程序相关的语义,你不能仅仅因为机器不介意就丢弃它。

评论

0赞 Tim Abell 11/16/2016
我认为这是一个被低估的答案。当计算机发现异常被吞噬时,它可能不会放慢太多速度,但是当我在处理别人的代码时,我遇到了它,它会阻止我停下来,同时我计算是否遗漏了一些我不知道的重要内容,或者实际上没有理由使用这种反模式。
6赞 Jason Punyon 4/8/2009 #7

我真的不明白你是如何控制你引用的代码中的程序流的。除了 ArgumentOutOfRange 异常之外,您永远不会看到其他异常。(所以你的第二个捕获条款永远不会被击中)。你所要做的就是使用一个极其昂贵的投掷来模仿一个if语句。

此外,您没有执行更险恶的操作,您只是抛出一个异常,纯粹是为了在其他地方捕获它以执行流控制。您实际上正在处理一个特殊情况。

1赞 Mesh 4/8/2009 #8

但是,您并不总是知道在调用的方法中会发生什么。你不会确切地知道异常是在哪里引发的。无需更详细地检查异常对象...。

27赞 Peter 4/8/2009 #9

对很多答案的第一反应:

你是在为程序员写作,也是最小惊讶原则

答案是肯定的!但是,如果只是并不总是更清楚。

这不应该令人惊讶,例如:除法 (1/x) 捕获 (divisionByZero) 对我来说(在 Conrad 和其他人)比任何都更清楚。这种编程不是预期的,这纯粹是传统的,事实上,仍然相关。也许在我的例子中,如果会更清楚。

但就此而言,DivisionByZero 和 FileNotFound 比 ifs 更清晰。

当然,如果它的性能较差并且每秒需要数以百万计的时间,您当然应该避免它,但我仍然没有读到任何充分的理由来避免整体设计。

就最小惊讶原则而言:这里存在循环推理的危险:假设整个社区使用糟糕的设计,这种设计将成为预期的!因此,该原则不能成为圣杯,应谨慎考虑。

正常情况下的例外情况,您如何定位异常(即特殊)情况?

在许多反应中。像这样闪耀着低谷。只要抓住他们,不是吗?你的方法应该很清楚,有据可查,并且遵守合同。我不明白这个问题,我必须承认。

对所有异常进行调试:相同,有时只是这样做,因为不使用异常的设计很常见。我的问题是:为什么它首先很常见?

评论

2赞 Lightman 10/30/2015
1) 你总是在打电话前检查吗?2) 你是否将每个除法操作包装成一个 try-catch 块来捕捉?3) 您在 catch 块中放入什么逻辑来恢复?x1/xDivideByZeroExceptionDivideByZeroException
2赞 0x6C38 5/28/2016
除了 DivisionByZero 和 FileNotFound 是不好的示例,因为它们是应被视为异常的例外情况。
1赞 Loduwijk 7/25/2017
对于以这种方式找不到的文件,没有什么比这里的人们吹捧的“反例外”更“例外”了。 可以跟一个捕获的 FileNotFound,并正常处理 FileNotFound 异常;没有崩溃,让我们让最终用户的体验更好,而不是更糟。你可能会说,“但是,如果这不是第一次运行,而且他们每次都这样做呢?至少应用程序每次都能运行,而不是每次启动都崩溃!在 1 到 10 的“这太可怕了”中:“首次运行”每次启动 = 3 或 4,每次启动都崩溃 = 10。openConfigFile();{ createDefaultConfigFile(); setFirstAppRun(); }
0赞 Vala 1/6/2018
您的示例是例外。不,您并不总是在打电话之前检查,因为通常没问题。例外情况是它不好的情况。我们在这里谈论的不是惊天动地,而是例如,对于给定随机数的基本整数,只有 1/4294967296 无法进行除法。这是例外情况,例外情况是处理这个问题的好方法。但是,您可以使用异常来实现等效于语句,但这很愚蠢。x1/xxswitch
4赞 kender 4/8/2009 #10

假设您有一个执行一些计算的方法。它必须验证许多输入参数,然后返回一个大于 0 的数字。

使用返回值来表示验证错误,这很简单:如果方法返回的数字小于 0,则发生错误。如何判断哪个参数没有验证?

我记得在我的 C 时代,很多函数都返回了这样的错误代码:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

等。

它真的不如抛出和捕捉异常可读性吗?

评论

0赞 Isak Savo 4/8/2009
这就是为什么他们发明了枚举:)但神奇的数字是一个完全不同的话题。en.wikipedia.org/wiki/......
0赞 Mark E. Haase 7/3/2013
很好的例子。我正要写同样的东西。@IsakSavo:如果方法需要返回某些含义值或对象,则枚举在这种情况下没有帮助。例如,getAccountBalance() 应该返回一个 Money 对象,而不是一个 AccountBalanceResultEnum 对象。许多 C 程序都有类似的模式,其中一个哨兵值(0 或 null)表示错误,然后您必须调用另一个函数来获取单独的错误代码,以确定错误发生的原因。(MySQL C API 是这样的。
4赞 Gambrinus 4/8/2009 #11

因为代码很难读,调试起来可能会有麻烦,长时间修复bug时会引入新的bug,在资源和时间方面更昂贵,如果你在调试代码时,调试器会在每次异常发生时停止;)

2赞 Patrick 4/8/2009 #12

通常,在低级别处理异常本身并没有什么问题。异常是一条有效消息,它提供了大量详细信息,说明无法执行操作的原因。如果你能处理它,你应该处理它。

一般来说,如果您知道故障的可能性很高,您可以检查...你应该做检查...即 if(obj != null) obj.method()

就您而言,我对 C# 库不够熟悉,无法知道日期时间是否有一种简单的方法来检查时间戳是否越界。如果是这样,只需调用 if(.isvalid(ts)) 否则你的代码基本上没问题。

所以,基本上它归结为哪种方式可以创建更干净的代码......如果防范预期异常的操作比仅处理异常更复杂;而不是你有我的许可来处理异常,而不是到处创建复杂的守卫。

评论

0赞 jasonnerothin 4/8/2009
补充点:如果您的 Exception 提供了失败捕获信息(例如“Param getWhatParamMessedMeUp()”),它可以帮助您的 API 用户就下一步做什么做出正确的决定。否则,您只是为错误状态命名。
9赞 James Koch 4/8/2009 #13

性能如何?在对 .NET Web 应用进行负载测试时,我们最多只能为每个 Web 服务器 100 个模拟用户,直到我们修复了一个常见的异常,并且该数字增加到 500 个用户。

12赞 jasonnerothin 4/8/2009 #14

Josh Bloch 在 Effective Java 中广泛地讨论了这个话题。他的建议很有启发性,也应该适用于 .NET(细节除外)。

特别是,例外情况应用于特殊情况。造成这种情况的原因主要与可用性有关。为了使给定方法具有最大可用性,其输入和输出条件应受到最大限度的约束。

例如,第二种方法比第一种方法更易于使用:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

无论哪种情况,您都需要检查以确保调用方正确使用您的 API。但在第二种情况下,您需要它(隐式)。如果用户没有阅读 javadoc,仍会抛出软异常,但是:

  1. 您无需将其记录下来。
  2. 你不需要测试它(取决于你的侵略性 单元测试策略是)。
  3. 您不需要调用方处理三个用例。

最根本的一点是,异常不应该被用作返回代码,主要是因为你不仅使你的 API 复杂化,而且使调用方的 API 也变得复杂。

当然,做正确的事是有代价的。代价是每个人都需要了解他们需要阅读和遵循文档。无论如何,希望情况是这样。

3赞 4/8/2009 #15

如果将异常处理程序用于控制流,则过于笼统和懒惰。正如其他人所提到的,如果您在处理程序中处理处理,您就会知道发生了一些事情,但究竟是什么?实质上,如果将 else 语句用于控制流,则将 exception 用于 else。

如果您不知道可能发生的状态,则可以将异常处理程序用于意外状态,例如,当您必须使用第三方库时,或者您必须捕获 UI 中的所有内容以显示漂亮的错误消息并记录异常。

但是,如果您确实知道可能出错的地方,并且您没有放置 if 语句或其他东西来检查它,那么您只是懒惰。让异常处理程序成为你知道可能发生的事情的包罗万象是懒惰的,它稍后会回来困扰你,因为你将试图根据一个可能错误的假设来修复异常处理程序中的情况。

如果你在异常处理程序中放置逻辑来确定到底发生了什么,那么你如果不将该逻辑放在 try 块中,那将是非常愚蠢的。

异常处理程序是最后的手段,因为当你用完了阻止某些事情出错的想法/方法,或者事情超出了你的控制能力时。例如,服务器已关闭并超时,您无法阻止引发该异常。

最后,预先完成所有检查可以显示您知道或期望会发生什么,并使其明确。代码的意图应该明确。你更愿意读什么?

评论

1赞 Peter 4/9/2009
根本不是真的:“ 本质上,如果你将异常用于控制流,则您正在将异常用于 else 语句。如果你用它来控制流,你就知道你到底抓住了什么,从不使用一般的捕捉,当然而是使用特定的捕捉!
2赞 simon 4/8/2009 #16

你可能有兴趣看看Common Lisp的条件系统,它是一种对正确完成的异常的泛化。因为你可以以受控的方式展开或不展开堆栈,所以你也可以“重启”,这非常方便。

这与其他语言的最佳实践没有太大关系,但它向你展示了在(大致)你所考虑的方向上,用一些设计思想可以做些什么。

当然,如果你像溜溜球一样在堆栈上上下弹跳,仍然需要考虑性能,但这是一个比大多数捕捉/抛出异常系统所体现的“哦,废话,让我们保释”的方法更普遍的想法。

2赞 Jörg W Mittag 4/9/2009 #17

我不认为使用 Exceptions 进行流量控制有什么问题。异常有点类似于延续,在静态类型语言中,异常比延续更强大,因此,如果您需要延续,但您的语言没有它们,您可以使用异常来实现它们。

好吧,实际上,如果你需要延续,而你的语言没有它们,你选择了错误的语言,你应该使用不同的语言。但有时你别无选择:客户端 Web 编程就是最好的例子——根本没有办法绕过 JavaScript。

举个例子:Microsoft Volta 是一个允许在简单的 .NET 中编写 Web 应用程序的项目,并让框架负责确定哪些位需要在哪里运行。这样做的一个结果是,Volta 需要能够将 CIL 编译为 JavaScript,以便您可以在客户端上运行代码。但是,存在一个问题:.NET 具有多线程,而 JavaScript 没有。因此,Volta 使用 JavaScript 异常在 JavaScript 中实现延续,然后使用这些延续实现 .NET Threads。这样,使用线程的 Volta 应用程序就可以被编译为在未经修改的浏览器中运行,而无需 Silverlight。

5赞 Sean 4/9/2009 #18

除了上述原因之外,不对流控制使用异常的一个原因是,它会使调试过程变得非常复杂。

例如,当我尝试在 VS 中跟踪错误时,我通常会打开“中断所有异常”。如果您使用异常进行流控制,那么我将定期中断调试器,并且必须继续忽略这些非异常异常,直到我找到真正的问题。这很可能会把人逼疯!!

评论

1赞 Peter 4/9/2009
我已经处理了一个 higer :调试所有异常:相同,这只是因为不使用异常的设计很常见。我的问题是:为什么它首先很常见?
0赞 Loduwijk 7/25/2017
那么你的答案基本上是“这很糟糕,因为 Visual Studio 有这个功能......”吗?我已经编程了大约 20 年,我什至没有注意到有一个“中断所有例外”选项。不过,“因为这个功能!”听起来像是一个软弱的理由。只需将异常跟踪到其来源即可;希望您使用的是一种使此操作变得简单的语言 - 否则您的问题出在语言功能上,而不是异常本身的一般用法。
3赞 Bryan Watts 8/21/2009 #19

您可以使用锤子的爪子来转动螺丝,就像您可以使用异常来控制流一样。这并不意味着这是该功能的预期用途。该语句表示条件,其预期用途控制流。if

如果您以非预期的方式使用某项功能,同时选择不使用为此目的设计的功能,则会产生相关成本。在这种情况下,清晰度和性能不会因真正的附加值而受到影响。使用例外对被广泛接受的陈述有什么好处?if

换一种说法:仅仅因为你可以并不意味着你应该这样做。

评论

1赞 Val 9/3/2013
你是说没有必要例外,在我们得到正常使用或使用执行不是故意的,因为它不是故意的(循环论证)?if
1赞 Bryan Watts 9/3/2013
@Val:异常是针对特殊情况的 - 如果我们检测到足够多的异常并处理它,我们就有足够的信息不抛出它并仍然处理它。我们可以直接进入处理逻辑,跳过昂贵的、多余的尝试/捕获。
0赞 Loduwijk 7/25/2017
按照这个逻辑,你最好不要有异常,并且总是执行系统退出而不是抛出异常。如果你想在退出之前做任何事情,那么做一个包装器并调用它。Java 示例: 然后直接调用它而不是抛出: 如果你的论点是合理的,那么这将是首选方法,不会有例外。你基本上是在说“异常的唯一原因是以不同的方式崩溃。public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }ExitHelper.cleanExit();
0赞 Bryan Watts 7/26/2017
@Aaron:如果我既抛出又捕捉到异常,我有足够的信息来避免这样做。这并不意味着所有的例外都会突然致命。我无法控制的其他代码可能会捕获它,这很好。我的论点仍然是合理的,即在同一上下文中抛出和捕获异常是多余的。我没有,也不会说所有例外都应该退出这一进程。
0赞 Loduwijk 7/29/2017
@BryanWatts承认。许多其他人说过,你应该只对任何无法恢复的东西使用异常,并且应该总是在异常时崩溃。这就是为什么很难讨论这些事情的原因;不仅有 2 种意见,而且很多。我仍然不同意你的看法,但不是强烈。有时,throw/catch together 是最易读、最可维护、最美观的代码;通常,如果您已经捕获了其他异常,因此您已经有了 try/catch,并且添加 1 或 2 个捕获比单独的错误检查更干净。if
24赞 necromancer 7/13/2013 #20

在异常之前,在 C 语言中,存在 和 可用于完成堆栈帧的类似展开。setjmplongjmp

然后,相同的构造被命名为:“Exception”。大多数答案都依赖于这个名称的含义来争论它的用法,声称例外是为了在特殊情况下使用。这从来都不是原著的意图。在某些情况下,您需要中断多个堆栈帧的控制流。longjmp

例外情况稍微通用一些,因为您也可以在同一堆栈帧中使用它们。这引发了我认为是错误的类比。Gotos 是一对紧密耦合的对(和 也是如此)。例外情况遵循松散耦合的发布/订阅,这要干净得多!因此,在同一个堆栈帧中使用它们与使用 s 几乎是一回事。gotosetjmplongjmpgoto

混淆的第三个来源涉及它们是选中的还是未选中的例外。当然,未经检查的异常似乎特别糟糕,可用于控制流以及许多其他事情。

然而,选中的异常对于控制流非常有用,一旦你克服了所有维多利亚时代的挂断并生活了一点。

我最喜欢的用法是一长段代码中的一系列,它一个接一个地尝试一件事,直到它找到它要找的东西。每件事 - 每个逻辑 - 都可能有 arbritrary 嵌套,所以 就像任何类型的条件测试一样。图案很脆。如果我以其他方式编辑或搞砸语法,那么就会有一个毛茸茸的错误。throw new Success()breakif-elseelse

使用线性化代码流。我使用本地定义的类 - 当然是检查的 - 这样如果我忘记捕获它,代码将无法编译。而且我没有抓住其他方法的 es。throw new Success()SuccessSuccess

有时我的代码会一个接一个地检查一件事,只有在一切正常的情况下才会成功。在这种情况下,我使用进行了类似的线性化。throw new Failure()

使用单独的函数会扰乱自然的区隔级别。所以解决方案不是最优的。出于认知原因,我更喜欢在一个地方有一两页代码。我不相信超精细划分的代码。return

除非有热点,否则 JVM 或编译器所做的事情与我不太相关。我不相信编译器有任何根本原因不检测本地抛出和捕获的异常,而只是将它们视为机器代码级别的非常有效的异常。goto

至于跨函数使用它们来控制流 - 即用于常见情况而不是特殊情况 - 我看不出它们如何比多次中断、条件测试、返回以涉足三个堆栈帧而不是仅仅恢复堆栈指针。

我个人不会在堆栈框架中使用这种模式,我可以看到它需要设计复杂性才能优雅地做到这一点。但谨慎使用应该没问题。

最后,关于令人惊讶的新手程序员,这不是一个令人信服的理由。如果你温柔地向他们介绍这种做法,他们就会学会爱上它。我记得C++曾经让C程序员感到惊讶和害怕。

评论

4赞 necromancer 7/13/2013
使用这种模式,我的大多数粗略函数在末尾都有两个小问题——一个用于成功,一个用于失败,这就是函数总结诸如准备正确的 servlet 响应或准备返回值之类的事情的地方。有一个地方可以做总结是很好的。-pattern 替代方案需要每个此类函数的两个函数。外部用于准备 servlet 响应或其他此类操作,内部用于执行计算。PS:一位英语教授可能会建议我在最后一段中使用“令人惊讶”而不是“令人惊讶”:-)return
1赞 supercat 9/3/2013 #21

有几种通用机制,通过这些机制,语言可以允许方法在不返回值的情况下退出并展开到下一个“捕获”块:

  • 让该方法检查堆栈帧以确定调用站点,并使用调用站点的元数据查找有关调用方法中块的信息,或调用方法存储其调用方地址的位置;在后一种情况下,检查调用方的调用方的元数据,以与直接调用方相同的方式进行确定,重复直到找到块或堆栈为空。这种方法对无异常情况增加的开销非常小(它确实排除了一些优化),但在发生异常时成本很高。trytry

  • 让该方法返回一个“隐藏”标志,用于区分正常返回和异常,并让调用方检查该标志并分支到“异常”例程(如果已设置)。此例程将 1-2 条指令添加到无异常情况中,但在发生异常时开销相对较小。

  • 让调用方将异常处理信息或代码放在相对于堆叠返回地址的固定地址。例如,对于 ARM,可以使用以下序列,而不是使用指令“BL 子例程”:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

要正常退出,子例程只需执行 或 ;如果出现异常退出,子程序将在执行返回或使用之前从 LR 中减去 4(取决于 ARM 变体、执行模式等)。如果调用方的设计不适应此方法,则此方法将出现非常严重的故障。bx lrpop {pc}sub lr,#4,pc

使用已检查异常的语言或框架可能会受益于使用上述 #2 或 #3 等机制处理这些异常,而使用未经检查的异常使用 #1 进行处理。尽管在 Java 中实现 checked 异常是相当令人讨厌的,但如果有一种方法可以让调用站点从本质上说“此方法被声明为抛出 XX,但我不希望它这样做;如果是这样,则作为“未选中”异常重新抛出。在以这种方式处理已检查异常的框架中,它们可以成为诸如解析方法之类的有效流控制手段,在某些情况下,这些方法可能很有可能失败,但失败应该返回与成功截然不同的信息。但是,我不知道有任何使用这种模式的框架。相反,更常见的模式是使用上述第一种方法(无异常情况的成本最低,但引发异常时成本高)用于所有异常。

1赞 gzak 9/25/2013 #22

一个美学原因:

尝试总是伴随着一个问题,而一个如果不一定伴随着其他问题。

if (PerformCheckSucceeded())
   DoSomething();

有了 try/catch,它变得更加冗长。

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

这 6 行代码太多了。

3赞 Lukas Eder 1/11/2015 #23

正如其他人多次提到的那样,最小惊讶原则将禁止您仅出于控制流目的过度使用异常。另一方面,没有一个规则是 100% 正确的,而且总是有一些例外是“恰到好处的工具”的情况——顺便说一句,它很像它本身,它以 Java 等语言的形式提供,这通常是跳出大量嵌套循环的完美方式,这并不总是可以避免的。gotobreakcontinue

下面的博客文章解释了一个相当复杂但也相当有趣的非本地用例:ControlFlowException

它解释了在 jOOQ(Java 的 SQL 抽象库)中,当满足某些“罕见”条件时,偶尔会使用此类异常来提前中止 SQL 渲染过程。

此类情况的示例包括:

  • 遇到过多的绑定值。某些数据库在其 SQL 语句中不支持任意数量的绑定值(SQLite:999、Ingres 10.1.0:1024、Sybase ASE 15.5:2000、SQL Server 2008:2100)。在这些情况下,jOOQ 会中止 SQL 渲染阶段,并使用内联绑定值重新渲染 SQL 语句。例:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }
    

    如果我们从查询 AST 中显式提取绑定值以每次都对其进行计数,那么对于没有此问题的 99.9% 的查询,我们将浪费宝贵的 CPU 周期。

  • 有些逻辑只能通过我们只想“部分”执行的 API 间接获得。UpdatableRecord.store() 方法生成一个 or 语句,具体取决于 的内部标志。从“外部”来看,我们不知道包含什么样的逻辑(例如乐观锁定、事件侦听器处理等),因此当我们在批处理语句中存储多条记录时,我们不想重复该逻辑,我们希望只生成 SQL 语句,而不是实际执行它。例:INSERTUPDATERecordstore()store()

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }
    

    如果我们将逻辑外部化为“可重用”的 API,可以自定义为选择不执行 SQL,我们将考虑创建一个相当难以维护、几乎不可重用的 API。store()

结论

从本质上讲,我们对这些非局部 s 的使用只是沿着 [Mason Wheeler][5] 在他的回答中所说的:goto

“我刚刚遇到了一个我现在无法正确处理的情况,因为我没有足够的上下文来处理它,但打电话给我的例程(或调用堆栈中更上层的东西)应该知道如何处理它。”

与它们的替代方案相比,这两种用法都相当容易实现,允许我们重用各种逻辑,而无需从相关内部重构它。ControlFlowExceptions

但是,对于未来的维护者来说,这有点令人惊讶的感觉仍然存在。代码感觉相当微妙,虽然在这种情况下它是正确的选择,但我们总是不希望对本地控制流使用异常,因为很容易避免使用普通的分支。if - else

8赞 Vladimir 2/27/2015 #24

以下是我在博客文章中描述的最佳实践:

  • 引发异常以说明软件中的意外情况
  • 使用返回值进行输入验证
  • 如果您知道如何处理库抛出的异常,请在尽可能低的级别捕获它们
  • 如果出现意外异常,请完全放弃当前操作。不要假装你知道如何对付他们