上世纪90年代后期,我在使用例外作为流控制的代码库中做了很多工作。它实现了一个有限状态机来驱动电话应用程序。最近,我想起那些日子,因为我一直在做MVC Web应用程序。

它们都具有Controller,它们决定下一步要去哪里并将数据提供给目标逻辑。来自老式电话域的用户操作(如DTMF音)已成为操作方法的参数,但他们没有返回ViewResult之类的东西,而是扔了StateTransitionException。动作方法是void函数。我不记得我对这个事实所做的所有事情,但是我一直犹豫要记住多少东西,因为自从15年前从事这项工作以来,我再也没有在生产代码中见过任何其他工作。我以为这是所谓的反模式的迹象。

是这种情况,如果是这样,为什么?

更新:当我问问题,我已经想到了@MasonWheeler的答案,因此我选择了最能增加我的知识的答案。我认为他也是一个正确的答案。

评论

相关问题:programmers.stackexchange.com/questions/107723

否。在Python中,将异常用作控制流被视为“ pythonic”。

如果我要用Java做这样的事情,我当然不会抛出异常。我将从一些非异常,非错误,可抛出的层次结构中派生。

为了增加现有的答案,以下是对我有用的简短指导:-切勿对“幸福之路”使用例外。满意的路径既可以是整个请求(对于Web),也可以是一个对象/方法。当然,所有其他理智规则仍然适用:)

异常不是总是控制应用程序的流程吗?

#1 楼

在Ward的Wiki上对此进行了详细的讨论。通常,在控制流中使用异常是一种反模式,在许多值得注意的情况下-特定于语言(例如Python)的咳嗽异常咳嗽。 ,这是一种反模式:


本质上,异常是复杂的GOTO语句
使用异常进行编程,因此导致更难以阅读和理解代码/>大多数语言都具有现有的控制结构,这些结构旨在解决您的问题而无需使用异常
对于现代编译器来说,效率的论点往往无济于事,现代编译器通常在不将异常用于控制流的假设下进行优化。 br />
阅读Ward Wiki上的讨论,以获取更深入的信息。

评论


您的答案听起来好像在所有情况下异常都是有害的,而问题的重点是作为流控制的异常。

–whatsisname
13年4月4日在22:46

@MasonWheeler的区别是for / while循环清楚地包含其流控制更改,并使代码易于阅读。如果在代码中看到for语句,则不必尝试找出哪个文件包含循环的结尾。 Goto的不错,因为有些上帝说他们是不错的,它们之所以糟糕,仅仅是因为它们比循环结构更难遵循。异常是相似的,不是不可能,但很难使异常混淆。

– Bill K
13年5月5日,0:02

@BillK,然后争论这一点,不要对异常是如何得到的做出简单的陈述。

–温斯顿·埃韦特(Winston Ewert)
13年5月5日在0:14

好的,但认真的说,服务器端和应用程序开发人员在JavaScript中使用空的catch语句掩埋错误是怎么回事?这是一个令人讨厌的现象,花费了我很多时间,而且我不知道如何不厌其烦地问。错误是你的朋友。

–埃里克·雷彭(Erik Reppen)
13年5月5日在4:43

@mattnz:if和foreach也是复杂的GOTO。坦率地说,我认为与goto的比较没有帮助;这几乎是一个贬义词。使用GOTO并不是本质上有害的-它有实际的问题,并且异常可能会共享,但可能不会。听到这些问题会更有帮助。

–Eamon Nerbonne
2015年1月11日在12:12



#2 楼

专为异常设计的用例是“我此时遇到了无法正确处理的情况,因为我没有足够的上下文来处理它,而是调用了我的例程(或进一步调用的例程堆栈)应该知道如何处理。”

第二个用例是“我刚遇到一个严重的错误,现在退出该控制流以防止数据损坏或其他损坏更重要。比尝试继续前进更重要。“

如果出于这两个原因之一不使用异常,则可能有更好的方法。

评论


但是,这并不能回答问题。它们的设计无关紧要。唯一相关的是为什么使用它们进行控制流不好,这是您没有涉及的主题。例如,C ++模板是为一件事而设计的,但是可以很好地用于元编程,这是设计人员从未想到的用途。

–托马斯·博尼尼(Thomas Bonini)
13年5月5日在1:26

@Krelp:设计师从未预料到很多事情,例如偶然获得了图灵完整的模板系统! C ++模板在这里几乎不是一个很好的例子。

–梅森·惠勒
13年5月5日在2:33

@Krelp-对于元编程,C ++模板并非“完美”。在您正确使用它们之前,它们是一场噩梦,然后,如果您不是模板天才,它们就会倾向于只写代码。您可能想选择一个更好的例子。

–迈克尔·科恩(Michael Kohne)
13年5月5日在2:39

异常尤其会损害功能的组成,并且通常会损害代码的简洁性。例如。当我没有经验的时候,我在项目中使用了异常,这很长时间以来引起了麻烦,因为1)人们必须记住要捕获它,2)您不能编写const myvar = TheFunction,因为必须尝试创建myvar -catch,因此不再恒定。这并不意味着我不会在C#中使用它,因为无论出于何种原因它都已成为C#的主流,但是我还是想尽办法减少它们。

– Hi-Angel
16-4-18在12:26



@AndreasBonini它们用于事物的设计/意图是因为它们通常会导致编译器/框架/运行时实现决策与设计保持一致。例如,引发异常比简单的返回要“昂贵得多”,因为它收集旨在帮助某人调试代码的信息,例如堆栈跟踪等。

– Binki
18年1月1日在15:25

#3 楼

异常与Continuations和GOTO一样强大。它们是通用控制流构造。

在某些语言中,它们是唯一的通用控制流构造。例如,JavaScript既没有Continuations也没有GOTO,甚至没有正确的尾部调用。因此,如果要在JavaScript中实现复杂的控制流,则必须使用Exceptions。

Microsoft Volta项目是一个(现已停产的)研究项目,用于将任意.NET代码编译为JavaScript。 .NET具有Exceptions,其语义不能完全映射到JavaScript,但更重要的是,它具有Threads,您必须以某种方式将其映射到JavaScript。 Volta通过使用JavaScript异常实现Volta Continuations来做到这一点,然后根据Volta Continuations实现所有.NET控制流构造。他们不得不使用Exceptions作为控制流,因为没有其他足够强大的控制流构造。

您提到了状态机。 SM通过适当的尾部调用实现起来很简单:每个状态都是一个子例程,每个状态转换都是一个子例程调用。 SM还可以通过GOTO或协程或Continuations轻松实现。但是,Java没有这四个中的任何一个,但是有例外。因此,将它们用作控制流是完全可以接受的。 (实际上,正确的选择可能是使用具有适当控制流构造的语言,但有时您可能会被Java所束缚。)

评论


如果只有适当的尾部调用递归,也许我们可以使用JavaScript在客户端Web开发中独占followed头,然后再扩展到服务器端和移动设备,成为野火之类的最终泛平台解决方案。 las,但事实并非如此。该死的我们一流的功能,闭包和事件驱动的范例还不够。我们真正需要的是真实的东西。

–埃里克·雷彭(Erik Reppen)
13年5月5日在3:50

@ErikReppen:我意识到您只是在讽刺,但是。 。 。我真的不认为我们“已经用JavaScript主导了客户端Web开发”这一事实与该语言的功能无关。它在那个市场上处于垄断地位,因此能够摆脱许多讽刺无法消除的问题。

–ruakh
13年5月5日在6:51

@谢恩:这些不是普遍的。就其中之一而言,不可能实现所有控制流。您不能仅使用if来实现异常或线程或GOTO。实际上,带有if和for的语言甚至都不是图灵完备的! (您说的是,不需要异常,那么,您将展示如何在ECMAScript中仅使用子例程调用来实现线程(如果有,而有则需要)。并请注意,Volta是针对ES3编写的,因此没有WebWorkers,没有Promises,没有适当的Tailcalls。

–‐Jörg W Mittag
2014年11月14日下午3:24

好,是的,您可能对。但是,C语言形式的“ For”在使用中时会很尴尬,而while则可以与if一起使用,以实现一个有限状态机,然后可以模拟所有其他流控制形式。再说一次,用臭气熏天的方式编码,但是,是的。再说一次,goto被认为是有害的。 (我也认为,在非特殊情况下使用异常也是如此)。请记住,存在完全有效且功能强大的语言,它们既不提供goto也不提供异常,并且工作正常。

– Shayne
2014年11月19日4:38



我很好奇如何使用异常来实现延续..

–hasen
15年1月11日在8:50

#4 楼

正如其他人多次提到的那样(例如在此Stack Overflow问题中),最小惊讶原则将禁止您仅出于控制流目的而过度使用异常。另一方面,没有规则是100%正确的,并且在某些情况下,例外是“恰好是正确的工具”-顺便说一句,就像goto本身一样,以breakcontinue的形式提供,例如Java,通常是跳出嵌套嵌套循环(并非总是可以避免的)的完美方法。

以下博客文章解释了非嵌套循环的相当复杂但也很有趣的用例。本地ControlFlowException


http://blog.jooq.org/2013/04/28/rare-uses-of-a-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中明确提取绑定值以对它们进行计数每次,我们都将浪费宝贵的CPU周期用于不遭受此问题困扰的99.9%的查询。


某些逻辑只能通过我们仅希望“部分”执行的API间接使用。 UpdatableRecord.store()方法根据INSERT的内部标志生成UPDATERecord语句。从“外部”,我们不知道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()逻辑外部化为“可重用” API可以自定义为不执行SQL的代码,我们将考虑创建一个很难维护且几乎不可重用的API。


结论

本质上,我们对这些非本地goto的用法与梅森·惠勒(Mason Wheeler)在回答中所说的大致相同:
此时,由于我没有足够的上下文来处理它,因此正确地进行了处理,但是调用我的例程(或调用堆栈中更高级的例程)应该知道如何处理它。“


与替代产品相比,ControlFlowExceptions的两种用法都非常易于实现,从而使我们能够重用广泛的逻辑,而无需从相关内部结构中进行重构。给未来的维护者remai带来一些惊喜ns。代码让人感觉很精致,虽然在这种情况下它是正确的选择,但我们始终不希望对本地控制流使用异常,因为可以避免通过if - else使用普通分支,这很容易避免。

#5 楼

将异常用于控制流通常被认为是一种反模式,但是存在异常(没有双关语)。

已经有上千次了,异常是为特殊条件而设计的。数据库连接断开是一种特殊情况。用户不能在只能输入数字的输入字段中输入字母。

软件中的错误,导致使用非法参数调用函数,例如null在不允许的情况下是例外情况。

通过对非常规情况使用异常,您正在为要解决的问题使用不合适的抽象。但是也可能会导致性能下降。某些语言或多或少具有高效的异常处理实现,因此,如果您选择的语言没有高效的异常处理,那么在性能方面可能会非常昂贵*。

其他语言,例如Ruby ,对于控制流具有类似异常的语法。特殊情况由raise / rescue运算符处理。但是您可以将throw / catch用于类似异常的控制流构造**。

因此,尽管通常不将异常用于控制流,但是您选择的语言可能还有其他成语。
*使用性能代价高昂的异常的示例:我曾经被设置为优化性能较差的ASP.NET Web窗体应用程序。原来,一张大桌子的渲染在大约30分钟后调用了int.Parse()。平均一页上有一千个空字符串,大约一千个例外情况正在处理中。通过用int.TryParse()替换代码,我节省了一秒钟!对于每个页面请求!

**对于使用其他语言来使用Ruby的程序员来说,这可能会非常混乱,因为throwcatch都是与许多其他语言中的异常相关联的关键字。

评论


+1表示“通过对异常情况使用异常,您正在为要解决的问题使用不合适的抽象。”

–乔治
18年4月4日在21:34

#6 楼

在不使用异常的情况下完全可以处理错误情况。有些语言,尤其是C语言,甚至没有例外,人们仍然设法使用它创建非常复杂的应用程序。异常之所以有用的原因是,它们使您可以在同一代码中简洁地指定两个本质上独立的控制流:一个发生错误,一个错误则没有。没有它们,您最终将在整个地方得到代码,如下所示:状态等。通常人们会指出“昂贵的”异常处理的方式,如果您不使用异常,则忽略如上需要添加的所有其他if语句。

在处理错误或其他“例外情况”时,这种情况最常发生。在我看来,如果您在其他情况下开始看到类似这样的样板代码,则对于使用异常有一个很好的论据。根据情况和实现,我可以看到状态机中有效地使用了异常,因为您有两个正交控制流:一个用于更改状态,另一个用于更改状态中发生的事件。

但是,这些情况很少见,如果您要对该规则做一个例外(双关语意),那么您最好准备展示它相对于其他解决方案的优越性。没有这种理由的偏差正确地称为反模式。

评论


只要那些if语句仅在“例外”情况下失败,现代CPU的分支预测逻辑就可以使其成本微不足道。这是宏可以真正提供帮助的地方,只要您小心且不要在宏中做太多事情即可。

–詹姆斯
13年5月5日,0:25

#7 楼

编程是关于工作的。
我认为回答这个问题的最简单方法是了解OOP多年来取得的进步。在OOP中完成的所有工作(以及大多数编程范例)都是围绕需要完成的工作进行建模的。知道怎么做,所以您帮我做。”
这带来了一个困难:当被调用的方法通常知道如何进行工作(但并非总是如此)时,会发生什么?我们需要一种沟通的方式“我确实想帮助您,但我确实做到了,但是我做不到。”
进行沟通的早期方法是简单地返回“垃圾”值。也许您期望一个正整数,所以被调用的方法返回一个负数。完成此操作的另一种方法是在某个位置设置错误值。不幸的是,这两种方法都导致样板让我在这里检查以确保所有的原始代码。随着事情变得越来越复杂,这个系统也崩溃了。您想用水管工修理水槽,所以他来看看。如果他只告诉您“对不起,我无法解决。它已损坏。”这不是很有用。地狱,如果他看一眼,离开,然后给你发一封信说他无法解决这个问题,那就更糟了。现在,您必须检查您的邮件,甚至不知道他没有执行您想要的操作。
您想要让他告诉您,“看,我无法修复它,因为它看起来像您的泵不能正常工作。“
利用此信息,您可以得出结论,希望电工检查问题。也许电工会找到与木匠有关的东西,您需要让木匠修复它。
哎呀,您甚至可能都不知道需要电工,您可能也不知道需要谁。您只是家庭维修业务的中层管理人员,而您的关注点正在不断扩大。因此,您告诉您是问题的负责人,然后他告诉电工将其解决。水管工不需要了解电工-他甚至不需要知道链上有人可以解决问题。他只是报告遇到的问题。
...是反模式吗?
好,所以了解异常的要点是第一步。接下来是了解什么是反模式。
要获得反模式的资格,它需要

解决问题
具有明确的负面影响

第一点很容易满足-系统起作用了,对吧?
第二点很粘。将异常用作常规控制流的主要原因是不好,因为这不是它们的目的。程序中任何给定的功能都应该具有相对明确的目的,而选择该目的会导致不必要的混乱。
但这并不是最终的危害。这是一种糟糕的做事方式,而且很怪异,但是是一种反模式?不,只是...奇怪。

评论


有一个不好的结果,至少在提供完整堆栈跟踪的语言中。由于代码是经过大量内联代码优化的,因此实际的堆栈跟踪与开发人员希望看到的代码有很大不同,因此生成堆栈跟踪的成本很高。过度使用异常对此类语言(Java,C#)的性能非常不利。

– maaartinus
17年1月6日在4:03

“这是一种糟糕的做事方式”-将其归类为反模式还不够吗?

–Maybe_Factor
18年4月5日在6:30

@Maybe_Factor根据蚂蚁模式的定义,不。

–MirroredFate
18年4月6日在17:31

#8 楼

在Python中,异常用于生成器和迭代终止。 Python具有非常有效的try / except块,但实际上引发异常会产生一些开销。

由于缺少多级中断或Python中的goto语句,我有时会使用异常:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error


评论


如果您必须在深层嵌套的语句中的某处终止计算并转到一些常见的延续代码,则很可能可以将这个特定的执行路径分解为一个函数,并用goto代替return。

– 9000
13年5月5日,0:40

@ 9000我正要评论完全相同的事情...继续尝试:请限制为1或2行,不要尝试:#做很多事情。

– Wim
13年5月5日在3:57

@ 9000,在某些情况下,可以肯定。但是,当该过程是一个连贯的线性过程时,您将无法访问局部变量,并且将代码移到另一个位置。

– Gahooa
13年3月14日在5:45

@gahooa:我曾经像你一样思考。这表明我的代码结构不良。当我多加思考时,我注意到本地上下文可以被弄乱,整个混乱变成了简短的函数,几乎没有参数,很少的代码行和非常精确的含义。我从不回头。

– 9000
13年3月14日在5:52



#9 楼

让我们来勾画这样的Exception用法:

算法递归搜索直到找到东西。因此,从递归返回时,必须检查是否找到了结果,然后返回,否则继续。并从某个递归深度反复返回。

除了需要一个额外的布尔值found(要打包在一个类中,否则可能只返回一个int),对于递归深度,发生同样的后遗症。因此,对我来说,这似乎是一种非goto型,更直接,更合适的编码方式。不需要,很少使用,也许是不好的风格,但是要点。与Prolog切割操作相当。

评论


嗯,是为真正相反的立场还是为不足或简短的论证而投反对票?我真的很好奇

–乔普·艾根(Joop Eggen)
14-10-20在21:09

我正面临着这样的困境,希望您能看到您对以下问题的看法?递归使用异常来累积顶级异常的原因(消息)codereview.stackexchange.com/questions/107862/…

– CL22
2015年10月19日在14:36

@Jodes我刚刚读过您有趣的技术,但是现在我很忙。希望其他人可以阐明她/他的观点。

–乔普·艾根(Joop Eggen)
15年10月19日在15:01