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


192

为了避免我可能会使用Google的所有标准答案,我将提供一个示例,您可以随意攻击。

C#和Java(和太多的人)有很多类型的一些“溢出”的行为我不喜欢在所有(如type.MaxValue + type.SmallestValue == type.MinValue例如: int.MaxValue + 1 == int.MinValue)。

但是,从我的恶毒天性来看,我会通过将这种行为扩展为“ Overridden” DateTime类型来加重这种伤害。(我知道DateTime它在.NET中是密封的,但是出于这个示例的缘故,我使用的伪语言与C#完全一样,但DateTime没有密封)。

覆盖的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. 履行您的方法合同

射死我


2
我有+1的感觉。除了性能之外,避免控制流异常的唯一很好的理由是,调用者代码将更易于读取返回值。

4
是:如果发生某件事,则返回-1,如果发生其他事情,则返回-2,以此类推...确实比异常更具可读性吗?
kender

1
令人遗憾的是,由于说实话而受到否定的声誉:您的示例无法用if语句编写。(这并不是说它是正确的/完整的。)
Ingo,

8
我会说,有时候抛出异常可能是您唯一的选择。例如,我有一个通过查询数据库来初始化其内部状态的业务组件。有时,数据库中没有适当的数据可用。在构造函数中引发异常是有效取消对象构造的唯一方法。在类的合同中(在我的情况下为Javadoc)中清楚地说明了这一点,因此我没有问题,客户端代码可以(并且应该)在创建组件时捕获该异常并从那里继续。
Stefan Haberl

1
由于您提出了假设,因此您有责任提出确凿的证据/理由。对于初学者来说,名字一个原因,为什么你的代码是优于短得多,自我记录if的语句。您会发现这很难。换句话说:您的前提是有缺陷的,因此您从中得出的结论是错误的。
康拉德·鲁道夫

Answers:


169

您是否曾经尝试过在正常操作过程中调试每秒产生五个异常的程序?

我有。

该程序非常复杂(它是一台分布式计算服务器),并且对该程序的一侧进行少量修改很容易在完全不同的地方破坏某些内容。

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

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

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


13
示例:调试.net程序时,我从Visual Studio启动它,并要求VS打破所有异常。如果您将异常作为预期的行为来依靠,那么我将无法再这样做(因为它会中断5次/秒),而定位有问题的代码部分要复杂得多。
布兰恩

15
+1表示您不想创建异常的干草堆来查找实际的异常情况。
格兰特·瓦格纳

16
根本没有得到这个答案,我认为人们在这里误解了它与调试完全无关,而与设计无关。恐怕这是纯粹形式的循环推理。您的意思实际上就是前面所说的问题之外的问题
Peter

11
@Peter:在不破坏异常的情况下进行调试是很困难的,并且如果有很多异常设计的话,捕获所有异常会很痛苦。我认为使调试困难的设计几乎被破坏了(换句话说,设计与调试有关,IMO)
Brann

7
即使忽略了我要调试的大多数情况并不与抛出的异常相对应的事实,您问题的答案仍然是:“按类型”,例如,我将告诉调试器仅捕获AssertionError或StandardError或执行某些操作对应发生的坏事。如果您对此有疑问,那么如何进行日志记录-难道您不按级别和类别进行日志记录,以便您可以对它们进行过滤吗?您是否也认为这是个坏主意?

157

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

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

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


11
嘿,例外并不惊人!当您说“这是一个坏主意”,然后继续说“但在python中这是一个好主意”时,您有点自相矛盾。
hasen

6
我仍然完全不相信:1)效率不是问题,很多非bacht程序也不会在乎(例如用户界面)2)令人惊讶:就像我说的那样,只是因为它没有被使用而令人惊讶,但是问题仍然存在:为什么不首先使用id?但是,由于这是答案
彼得,彼得

4
+1实际上,我很高兴您指出Python和C#之间的区别。我不认为这是矛盾的。Python更加动态,并且以这种方式使用异常的期望也融入了语言中。它也是Python EAFP文化的一部分。我不知道哪种方法在概念上更纯净或更自洽,但是我很喜欢编写代码来实现他人期望的想法,这意味着在不同语言中使用不同的样式。
Mark E. Haase 2013年

13
goto当然,与with不同,异常可以正确地与您的调用堆栈和词法作用域进行交互,并且不会使堆栈或作用域混乱。
卢卡斯·埃德

4
实际上,大多数VM供应商期望异常并有效地处理它们。正如@LukasEder所指出的,异常与goto完全不同,因为它们是结构化的。
Marcin

29

我的经验法则是:

  • 如果您可以采取任何措施来从错误中恢复,请捕获异常
  • 如果错误是非常常见的错误(例如,用户尝试使用错误的密码登录),请使用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
回复:您的第二个示例-为什么不在Build之后将对BestMethodEver的调用放在try块中?如果Build()引发异常,则不会执行该异常,并且编译器感到满意。
Blorgbeard在

2
是的,最终可能会得到,但是考虑一个更复杂的示例,其中myInstance类型本身可以引发异常。方法范围内的其他符号也可以。您最终会遇到很多嵌套的try / catch块:(
cwap

您应该在catch块中进行异常转换(转换为适合抽象级别的异常类型)。仅供参考:“多重捕获”应该会进入
Java7。– jasonnerothin

仅供参考:在C ++中,您可以在尝试捕获不同的异常之后放置多个catch。
罗布

2
对于shrinkwrap软件,您需要捕获所有异常。至少要弹出一个对话框,说明该程序需要关闭,您可以在其中提交一个错误报告,这令人难以理解。
David Thornley,2009年

25

对许多答案的第一反应:

您是在为程序员和最小惊讶原则而写

当然!但是如果不是一直都不清楚的话。

它不应该令人惊讶,例如:除(1 / x)catch(divisionByZero)比对我而言(在康拉德(Conrad)等公司)更清晰。不会发生这种编程的事实纯属常规,并且确实仍然有意义。也许在我的示例中,如果更清晰。

但是,对于该问题,DivisionByZero和FileNotFound比ifs更清晰。

当然,如果它的性能较差并且需要每秒数以千计的时间,那么您当然应该避免使用它,但是我仍然没有阅读任何充分的理由来避免总体设计。

就最小惊讶原则而言:这里存在循环推理的危险:假设整个社区使用的是错误的设计,那么这种设计将是可以预期的!因此,该原则不能成为挑战,应该仔细考虑。

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

在许多反应中 像这样的低谷。赶上他们,不是吗?您的方法应清晰,有据可查,并应减少合同约定。我必须承认这个问题。

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


1
1)您总是x在打电话之前检查一下1/x吗?2)您是否将每个除法操作都包装到try-catch块中进行捕获DivideByZeroException?3)什么逻辑,你投入catch块才能恢复DivideByZeroException
Lightman

1
除DivisionByZero和FileNotFound之外,它们都是不好的例子,因为它们是例外情况,应视为例外。
0x6C38 '16

找不到文件的方式“过分例外”,就像这里的“反例外”人员在吹捧一样。openConfigFile();紧随其后的是捕获的FileNotFound,且{ createDefaultConfigFile(); setFirstAppRun(); }FileNotFound异常得到了妥善处理;没有崩溃,让我们让最终用户的体验更好,而不是更糟。您可能会说:“但是,如果这不是真正的第一次尝试,他们每次都能得到呢?” 至少该应用程序每次都运行,并且不会在每次启动时都崩溃!在1到10上,这“太糟糕了”:每个初创企业“首次运行” = 3或4,每个初创企业崩溃 =
10。– Loduwijk

您的示例是例外。不,您不一定总是x在致电之前进行检查1/x,因为通常没问题。例外情况是这种情况不正确。我们在这里不是在说惊天动地,但是例如对于给定了随机数的基本整数x,在4294967296中只有1不能进行除法。这是例外,并且例外是解决该问题的好方法。但是,您可以使用异常来实现等效的switch语句,但是这样做很愚蠢。
Thor84no

16

在例外之前,在C中,存在setjmplongjmp可以用于完成堆栈帧的类似展开。

然后,将相同的构造命名为:“ Exception”。而且大多数答案都依赖于该名称的含义来争论其用法,声称例外是在特殊情况下使用的。那从来不是原版的意图longjmp。在某些情况下,您需要打破许多堆栈框架之间的控制流。

异常稍微更一般些,因为您也可以在同一堆栈框架中使用它们。这与goto我认为是错误的类推。goto方法是紧耦合对(和因此是setjmplongjmp)。例外情况是松散耦合的发布/订阅更为简洁!因此,在同一堆栈帧中使用它们与使用gotos 几乎没有什么不同。

造成混乱的第三个原因是它们是检查异常还是未检查异常。当然,未经检查的异常似乎特别不适用于控制流和其他许多事情。

但是,一旦您克服了所有维多利亚时代的困扰,并且生活了一点,检查异常对于控制流程非常有用。

我最喜欢的用法是一段throw new Success()很长的代码片段序列,该序列尝试一件事接另一件事,直到找到要查找的内容。每件事-每一条逻辑-可能都有任意嵌套,因此不break存在任何条件测试。该if-else模式是脆。如果我else以其他方式修改了或弄乱了语法,那么就会出现毛毛虫。

使用throw new Success() 线性化代码流。我使用本地定义的Success类(当然已检查),因此,如果我忘记抓住它,则代码将无法编译。而且我不了解另一种方法Success

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

使用单独的功能会干扰分隔的自然水平。因此,return解决方案不是最佳的。由于认知原因,我更喜欢在一个地方放置一两个页面的代码。我不相信超细分代码。

除非有热点,否则JVM或编译器的工作与我无关。我无法相信编译器没有任何根本原因无法检测本地引发和捕获的异常,而只是goto在机器代码级别将它们视为非常有效的。

至于跨函数使用它们以进行控制流(即,对于普通情况而不是特殊情况),我看不出它们比多次中断,条件测试,通过三个堆栈框架返回而不是仅仅恢复的效率如何低堆栈指针。

我个人没有在整个堆栈框架上使用该模式,我可以看到它如何要求设计精巧才能做到优雅。但是少用它应该没问题。

最后,对于令人惊讶的原始程序员来说,这不是一个令人信服的理由。如果您将他们轻轻地介绍给实践,他们会学会喜欢它。我记得C ++曾经使C程序员感到惊讶和恐惧。


3
使用这种模式,我的大多数粗糙函数在末尾都有两个小陷阱-一个是“成功”,另一个是“失败”,在那儿,函数包装了诸如准备正确的servlet响应或准备返回值之类的东西。有一个地方可以做总结很好。该return-pattern替代将需要两个功能,为每一个这样的功能。一个外部人准备servlet响应或其他此类操作,一个内部人进行计算。PS:一位英语教授可能会建议我在最后
一段中

11

标准的警告是例外不是常规的,在例外情况下应使用例外。

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

请记住,您不仅为计算机而且为其他编码器编写代码。有一个与异常处理程序相关联的语义,您不能仅仅因为机器不在乎就放弃它。


我认为这是一个未被重视的答案。当计算机发现异常被吞没时,计算机的速度可能不会减慢很多,但是当我在处理别人的代码时,碰到了它,如果我错过了我没有做过的一些重要事情,它将使我无所适从不知道,或者实际上没有使用这种反模式的理由。
蒂姆·阿贝尔

9

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


8

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也是如此。

当然,做正确的事要付出代价。代价是每个人都需要了解他们需要阅读和遵循文档。希望情况确实如此。


7

我认为您可以将异常用于流控制。但是,这种技术还有另一面。创建异常是一件昂贵的事情,因为它们必须创建堆栈跟踪。因此,如果您想更多地使用“异常”,而不仅仅是表示特殊情况,则必须确保构建堆栈跟踪不会对性能产生负面影响。

减少创建异常的成本的最好方法是重写fillInStackTrace()方法,如下所示:

public Throwable fillInStackTrace() { return this; }

这样的异常将不会填充堆栈跟踪。


堆栈跟踪还要求调用者“知道”(即依赖于)堆栈中的所有Throwable。这是一件坏事。抛出适合于抽象级别的异常(服务中的ServiceException,Dao方法中的DaoException等)。如有必要,请翻译。
jasonnerothin

5

我没有真正看到您在引用的代码中如何控制程序流。除了ArgumentOutOfRange异常之外,您将再也看不到其他异常。(因此,您的第二个catch子句将永远不会被击中)。您要做的只是使用极其昂贵的抛出来模仿if语句。

另外,您不会执行更险恶的操作,您只是为了将异常捕获到其他地方以执行流控制而抛出异常。您实际上正在处理一个例外情况。


5

这是我在博客文章中描述的最佳做法:

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

4

由于代码难以阅读,因此调试时可能会遇到麻烦,很长一段时间后修复错误时,您将引入新的错误,在资源和时间方面更昂贵,并且如果调试代码和调试器在每个异常发生时都暂停;)


4

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

例如,当我试图追踪VS中的错误时,通常会打开“打破所有异常”。如果您将异常用于流控制,那么我将定期调试器,在我遇到真正的问题之前,必须一直忽略这些非异常。这很可能使某人发疯!!


1
我已经提出了一个更高的要求:对所有异常进行调试:都是一样的,因为不使用异常的设计很常见。我的问题是:为什么首先要常见?
彼得

因此,您的答案是否基本上是“不好,因为Visual Studio具有这一功能……”?我从事编程已有大约20年了,甚至没有注意到“打破所有例外”选项。不过,“因为这一功能!” 听起来像一个微弱的理由。只需跟踪异常源即可;希望您使用的语言可以简化此操作-否则您的问题在于语言功能,而不是异常本身的一般用法。
Loduwijk

3

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

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

我记得在我的C天中,很多函数都返回了如下错误代码:

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

等等

它真的不那么可读,然后引发并捕获异常吗?


这就是为什么他们发明枚举的原因:)但是魔术数字是一个完全不同的主题。.en.wikipedia.org
wiki/

很好的例子。我正要写同样的东西。@IsakSavo:如果期望该方法返回一些含义值或对象,则枚举在这种情况下没有帮助。例如,getAccountBalance()应该返回Money对象,而不是AccountBalanceResultEnum对象。许多C程序都有类似的模式,其中一个哨兵值(0或null)表示错误,然后您必须调用另一个函数以获取单独的错误代码,才能确定发生错误的原因。(MySQL C API就是这样。)
Mark E. Haase

3

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

如果您在选择不使用为此目的而设计的功能时意外地使用了该功能,则会产生相应的费用。在这种情况下,清晰度和性能会遭受损失,而没有真正的附加值。在广泛接受的if陈述中,使用例外能为您带来什么?

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


1
您是在说我们if正常使用后还是不需要执行,因为它不是故意的(循环参数),所以不需要例外吗?
2013年

1
@Val:异常是针对特殊情况的-如果我们检测到足以引发异常并对其进行处理的信息,则我们有足够的信息可以引发并仍然处理该异常。我们可以直接进入处理逻辑,而跳过昂贵的,多余的try / catch。
布莱恩·瓦茨

按照这种逻辑,您最好也没有异常,并且总是执行系统退出操作而不是抛出异常。如果您想在退出之前做任何事情,请制作一个包装器并调用它。Java示例:public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }然后调用它而不是抛出该错误:ExitHelper.cleanExit();如果您的论点是正确的,那么这将是首选方法,并且不会有例外。您基本上是在说“发生异常的唯一原因是以不同的方式崩溃”。
Loduwijk

@Aaron:如果我都抛出并捕获异常,则我有足够的信息来避免这样做。这并不意味着所有例外都会突然致命。我不控制的其他代码可能会捕获它,这很好。我的观点仍然是正确的,那就是在相同的上下文中抛出和捕获异常是多余的。我没有,也没有声明所有异常都应该退出流程。
布赖恩·瓦茨

@BryanWatts已确认。许多其他人已经说过,您应该只对无法恢复的任何事物使用异常,并且从广义上讲,应该总是在异常崩溃。这就是为什么很难讨论这些事情的原因;不仅有2种意见,而且还有很多。我仍然不同意您的看法,但并不完全同意。有时将最容易阅读,可维护,最漂亮的代码一起投掷/捕获;通常,如果您已经在捕获其他异常,则已经发生try / catch,并且添加1个或2个以上的catch比使用单独的错误检查更干净,通常会发生这种情况if
罗杜维克

3

正如其他人多次提到的那样,最小惊讶原则将禁止您仅将异常用于控制流目的。在另一方面,没有什么规则是100%正确的,总有那些情况下的例外是“恰到好处的工具” -就像goto自己,顺便说一句,这船的形式breakcontinue像Java,语言这通常是跳出严重嵌套循环的理想方法,而这并非总是可以避免的。

以下博客文章解释了一个非本地 的相当复杂但有趣的用例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()方法根据的内部标志生成一个INSERTor UPDATE语句Record。从“外部”开始,我们不知道其中包含哪种逻辑store()(例如,乐观锁定,事件监听器处理等),因此当我们在批处理语句中存储多个记录时,我们不想重复该逻辑,我们store()只想生成SQL语句,而不实际执行它。例:

    // 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());                
      }
    }

    如果我们已经将store()逻辑外部化为可以定制为执行SQL的“可重用” API ,那么我们将着眼于创建一个难以维护,几乎不可重用的API。

结论

本质上,我们对这些非本地gotos的用法与[Mason Wheeler] [5]在他的回答中所说的相似:

“我刚刚遇到一种情况,目前我无法正确处理,因为我没有足够的上下文来处理它,但是调用我的例程(或调用堆栈中更远的地方)应该知道如何处理它。”

ControlFlowExceptions与它们的替代方案相比,这两种用法都非常易于实现,从而使我们能够重用广泛的逻辑,而无需从相关内部结构中进行重构。

但是这种感觉让未来的维护者感到惊讶。代码让人感觉很精致,虽然在这种情况下它是正确的选择,但我们始终不希望对本地控制流不使用异常,因为这样很容易避免使用普通的分支方式if - else


2

通常,在低级别处理异常本身没有任何错误。异常是一条有效消息,其中提供了许多无法执行操作的详细信息。如果可以处理,就应该这样做。

通常,如果您知道可以检查的故障可能性很高,则应进行检查...即if(obj!= null)obj.method()

在您的情况下,我对C#库不够熟悉,无法知道日期时间是否可以轻松地检查时间戳是否超出范围。如果是这样,只需调用if(.isvalid(ts)),否则您的代码就可以了。

因此,基本上,归结为用哪种方式可以创建更简洁的代码...如果防范预期异常的操作比处理异常更复杂,那么它就可以了。而不是您有权处理该异常,而不是在各处创建复杂的防护措施。


要点:如果您的异常提供了故障捕获信息(如“ Param getWhatParamMessedMeUp()之类的吸气剂”),则它可以帮助您的API用户对下一步的工作做出正确的决定。否则,您只是给错误状态起一个名字。
jasonnerothin

2

如果您将异常处理程序用于控制流,那么您太笼统和懒惰。正如其他人提到的那样,如果您在处理程序中处理处理,您会知道发生了什么,但是究竟是什么呢?本质上,如果将异常用于控制流,则将异常用于else语句。

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

但是,如果您确实知道可能出了什么问题,并且没有放置if语句或其他东西来检查它,那么您就是在懒惰。允许异常处理程序成为您可能会发生的事情的全部包罗万象,这很懒惰,并且稍后会再次困扰您,因为您将尝试基于可能错误的假设来修复异常处理程序中的情况。

如果将逻辑放在异常处理程序中以确定确切发生了什么,那么如果不将该逻辑放入try块中,则会非常愚蠢。

异常处理程序是万不得已的方法,因为当您用尽了所有的想法/方法来阻止某些错误时,或者某些事情超出了您的控制范围时。就像,服务器已关闭并超时,您无法阻止该异常被抛出。

最后,预先完成所有检查将显示您知道或期望的事情,并使其明确。代码应明确意图。您宁愿阅读什么?


1
根本不正确:“本质上来说,如果将异常用于else语句,则将其用于控制​​流。”如果将其用于控制​​流,则将知道所捕获的内容而从不使用常规捕获,而是具体的当然之一!
彼得

2

您可能对研究Common Lisp的条件系统感兴趣,这是对正确完成的异常的一种概括。因为您可以以受控方式打开或不打开堆栈,所以也会获得“重新启动”,这非常方便。

这与其他语言的最佳实践并没有多大关系,但它向您展示了(大致)您所想到的方向上的某些设计思想可以做什么。

当然,如果您像溜溜球一样上下跳动,仍然需要考虑性能,但这是比大多数捕获/抛出异常系统所体现的“哦,废话,请保释”这种方法更为笼统的想法。


2

我认为使用Exceptions进行流控制没有任何问题。异常在某种程度上类似于延续,并且在静态类型的语言中,异常比延续更强大,因此,如果您需要延续但您的语言没有延续,则可以使用异常来实现它们。

好吧,实际上,如果您需要继续并且您的语言没有继续,您选择了错误的语言,您应该使用另一种。但是有时您别无选择:客户端Web编程是一个很好的例子–根本无法解决JavaScript。

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


2

审美原因之一:

尝试总是会带来一些麻烦,而if并不一定要伴随else。

if (PerformCheckSucceeded())
   DoSomething();

通过try / catch,它变得更加冗长。

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

那六行代码太多了。


1

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


1

我觉得您的榜样没有错。相反,忽略被调用函数引发的异常将是一种罪过。

在JVM中,抛出异常并不是那么昂贵,仅使用新的xyzException(...)创建异常即可,因为后者涉及堆栈遍历。因此,如果您事先创建了一些异常,则可以多次抛出它们而无需花费任何费用。当然,这种方式不能将数据与异常一起传递,但是我认为无论如何这都是一件坏事。


抱歉,这是完全错误的,布朗。这取决于条件。条件并非总是微不足道的。因此,一条if语句可能要花费数小时甚至数天甚至更长的时间。
Ingo 2009年

在JVM中。不比退货贵。去搞清楚。但是问题是,如果不是,那么您将如何在if语句中编写什么代码,以从正常代码中分辨出一个特例,即代码复制,因此该代码已经存在于被调用函数中。
Ingo 2009年

1
英戈(Ingo):一种特殊情况是您无法预期的。即您没有想到的程序员。所以我的规则是“编写不会抛出异常的代码” :)
Brann

1
我从不编写异常处理程序,我总是解决问题(除非因为无法控制错误代码而无法这样做)。我从不抛出异常,除非我编写的代码是供其他人使用的(例如库)。没有显示我的矛盾吗?
布兰恩

1
我同意您的看法,不要乱扔异常。但是可以肯定的是,“例外”是定义的问题。例如,如果String.parseDouble无法传递有用的结果,则抛出异常。它还应该做什么?返回NaN?非IEEE硬件呢?
Ingo 2009年

1

语言可以通过几种通用机制来允许方法退出而无需返回值并展开到下一个“ catch”块:

  • 让该方法检查堆栈框架以确定调用站点,并使用该调用站点的元数据查找有关try调用方法中的块的信息,或查找该调用方法存储其调用者地址的位置;在后一种情况下,检查呼叫者的呼叫者的元数据,以与直接呼叫者相同的方式确定,重复直到找到一个try块或堆栈为空。这种方法在没有例外的情况下增加了很少的开销(它确实排除了某些优化),但是在发生异常时开销很大。

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

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

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

要正常退出,子程序只需执行bx lrpop {pc}; 即可。在异常退出的情况下,子例程将在执行返回之前从LR减去4或使用sub lr,#4,pc(取决于ARM的变化,执行模式等)。如果调用者的设计不适合这种方法,则此方法将非常无法正常工作。

使用受检查的异常的语言或框架可能会受益于使用上述#2或#3之类的机制来处理那些语言或框架,而未检查的异常使用#1处理。尽管Java中检查异常的实现相当麻烦,但是如果有一种方法可以使调用站点说“本质上说,“此方法声明为抛出XX,但是我不希望这样做”,这并不是一个坏概念。如果这样做,则将其作为“未检查的”异常重新抛出。在以这种方式处理检查的异常的框架中,它们可能是流控制诸如解析方法之类的事情的有效方法,在某些情况下可能具有失败的可能性很高,但是失败应该返回与成功根本不同的信息。但是,没有意识到任何使用这种模式的框架。相反,更常见的模式是对所有异常使用上述第一种方法(无异常情况下的最低成本,而引发异常时的成本较高)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.