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

Pet*_*ter 184 language-agnostic exception

为了避免我可以用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.
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我认为在很多情况下,它们比if-structures更清晰,并且不会违反该方法的任何合同.

恕我直言"从不使用它们进行常规程序流程"反应,每个人似乎都没有那么好的建设,因为这种反应的力量可以证明是合理的.

还是我弄错了?

我已经阅读过其他帖子,处理各种特殊情况,但我的观点是,如果你们两个都是:

  1. 明确
  2. 尊重您方法的合同

拍我

Bra*_*ann 162

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

我有.

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

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

我的观点:如果你在正常情况下使用例外,你如何找到异常(即例外情况)?

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

  • 根本没有得到这个答案,我认为人们在这里误解它完全与调试无关,而是与设计有关.这是循环推理,因为我害怕它的形式.除了前面提到的问题,你的观点确实如此 (15认同)
  • +1指出你不想创建一个例外干草堆,在其中找到一个实际的特殊针. (14认同)
  • 示例:当我调试.net程序时,我从visual studio启动它,我要求VS打破所有异常.如果您依赖异常作为预期行为,我不能再这样做了(因为它会突破5次/秒),找到代码中有问题的部分要复杂得多. (12认同)
  • @Peter:在不破坏异常的情况下进行调试很困难,如果设计中存在大量异常,捕获所有异常都会很痛苦.我认为使调试变得困难的设计几乎部分被打破(换句话说,设计与调试,IMO有关) (9认同)
  • 甚至忽略了我想要调试的大多数情况与抛出的异常不对应的事实,你的问题的答案是:"按类型",例如,我会告诉我的调试器只捕获AssertionError或StandardError或者其他东西对应发生的坏事.如果你遇到问题,那么你如何做日志记录 - 你不是按级别和类别进行日志记录,这样你就可以过滤它们了吗?你觉得这也是个坏主意吗? (7认同)
  • 布兰恩:正如我试图说的那样(但也许不是很清楚),类型很重要.因此,NullReferencException可能是一个用于控制流的错误.但这并不意味着像StopIteration或MildWarning这样的东西会很糟糕. (4认同)
  • 您可以捕获流量控制的预期异常并在相同的try块之后捕获意外,我没有看到问题是诚实的. (3认同)
  • /我同意。我去过那里……我还在那里。并且需要射击在 CF 中为每个资源查找在内部抛出 FileNotFound 的人。 (2认同)
  • @Ken :假设我在这里和那里依赖 NullReferenceException 来处理我的控制流。您是否提倡在您的环境开发中禁用 nullref 异常捕获?如果这样做,如果引发意外(即,未用于常规控制流) nullref 异常怎么办?您根本没有任何方法可以轻松定位它(因为如果您捕获 nullref 异常,您将陷入无限循环中,由于“常规控制流原因”,每隔几毫秒就会引发一次 nullref 异常...... (2认同)
  • @Ken:不幸的是,大多数依赖异常的人都在重复使用标准异常。或者,更具体地说,它们让 CLR 引发这些异常(例如,它们不测试变量是否为空,并在需要时处理 nullref)。话虽这么说,即使有用户异常,我的观点仍然成立,除非每次需要引发异常时都依赖于不同的异常类型,这似乎也不太方便...... (2认同)

Ant*_*lev 152

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

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

但是,某些语言(特别是Python)使用异常作为流控制结构.例如,StopIteration如果没有其他项,则迭代器会引发异常.甚至标准语言结构(例如for)依赖于此.

  • 我从来没有说过在Python中这是一个好主意. (64认同)
  • 当然,与`goto`不同,异常正确地与您的调用堆栈和词法范围进行交互,并且不会使堆栈或范围陷入混乱. (12认同)
  • 嘿,异常并不令人惊讶!当你说"这是一个坏主意"然后继续说"但这在python中是个好主意"时,你有点自相矛盾. (11认同)
  • 我仍然不相信:1)效率除了问题之外,很多非游艇程序也不在乎(例如用户界面)2)令人惊讶:就像我说它只是令人惊讶的原因它没有被使用,但是问题仍然存在:为什么不首先使用id?但是,因为这是答案 (6认同)
  • +1实际上我很高兴你指出了Python和C#之间的区别.我不认为这是一个矛盾.Python更具动态性,并且以这种方式使用异常的期望被融入到语言中.它也是Python的EAFP文化的一部分.我不知道哪种方法在概念上更纯粹或更自洽,但我确实喜欢编写代码来执行其他人*期望*它所做的事情,这意味着不同语言中的不同风格. (4认同)
  • 实际上,大多数VM供应商都期望异常,并有效地处理它们.正如@LukasEder所指出的,异常与goto完全不同,因为它们是结构化的. (4认同)
  • 如果我没记错的话,Java中的迭代for语句(for(foo:bar))依赖于异常以及中止.无法保证将调用hasNext()而不会调用iirc. (2认同)
  • Ruby有一个`raise` /`rescue`对用于异常,完成堆栈跟踪等等,并且它有一个单独的`catch` /`throw`机制,用于日常的非本地控制流使用(标记的替换类型) break)你可以抛出任何类型的对象(一个用于指示哪个catch块感兴趣,另一个用于确定其返回值).因此,在可读性方面 - 我相信你可以让他们期待._Esonal_在Ruby中. (2认同)
  • 我不得不对这个答案中的不良信息投反对票。编译器供应商希望您尽可能多地使用必要或有用的语言构造;这就是这个结构存在的原因。至于*惊讶*,只有对不了解异常的初学者程序员来说才会感到惊讶。与所有语言功能一样,您只需要正确使用它即可;这甚至适用于“if”、“for”、函数和其他通用语言功能。 (2认同)

cwa*_*wap 26

我的经验法则是:

  • 如果您可以执行任何操作以从错误中恢复,请捕获异常
  • 如果错误是非常常见的(例如,用户尝试使用错误的密码登录),请使用returnvalues
  • 如果你无法做任何事情来从错误中恢复,那么保持它没有被捕获(或者在主捕获器中捕获它以对应用程序进行一些半正常的关闭)

我看到异常的问题是从纯语法的角度来看(我很确定性能开销很小).我不喜欢整个地方的试用块.

举个例子:

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

..另一个例子可能是当你需要使用工厂为句柄分配东西时,该工厂可能抛出异常:

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.. :(
Run Code Online (Sandbox Code Playgroud)

洙,就个人而言,我认为你应该保持罕见的错误条件异常(内存不足等),并使用returnvalues(valueclasses,结构或枚举)做你的错误检查来代替.

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

  • re:你的第二个例子 - 为什么不在Build之后将调用放到try块中的BestMethodEver?如果Build()抛出异常,它将不会被执行,并且编译器很高兴. (4认同)
  • 是的,这可能是你最终会得到的,但考虑一个更复杂的例子,其中myInstance类型本身可以抛出异常..方法范围中的其他方法也可以.你最终会得到很多嵌套的try/catch块:( (2认同)
  • 对于shrinkwrap软件,您需要捕获所有异常.至少提出一个对话框,说明程序需要关闭,这里有一些不可理解的东西,你可以发送错误报告. (2认同)

Pet*_*ter 24

对许多答案的第一反应:

你是为程序员写的,也是最不惊讶的原则

当然!但是,如果不是一直都不清楚.

它应该不是令人惊讶的例如:除(对于我来说)(在康拉德和其他人处),除(1/x)捕获(divisionByZero)比任何更明确.不期望这种编程的事实纯粹是传统的,实际上仍然是相关的.也许在我的例子中,if会更清楚.

但是DivisionByZero和FileNotFound比ifs更清楚.

当然,如果它的性能较差并且每秒需要大量的时间,你当然应该避免它,但我仍然没有找到任何有理由避免全部设计.

至于最小惊讶的原则:这里存在循环推理的危险:假设整个社区使用了糟糕的设计,这种设计将成为预料之中!因此,原则不能是一个圣杯,应该仔细斟酌.

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

在很多反应中...... 像这样闪耀着低谷.抓住他们,不是吗?你的方法应该清晰,记录良好,并且能够签订合同.我必须承认,我没有得到这个问题.

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

  • 1) 在调用 `1/x` 之前你总是检查 `x` 吗?2) 您是否将每个除法操作包装到 try-catch 块中以捕获 `DivideByZeroException` ?3) 你在 catch 块中放入了什么逻辑来从 `DivideByZeroException` 中恢复? (2认同)
  • 除了 DivisionByZero 和 FileNotFound 之外,它们都是不好的例子,因为它们是例外情况,应该被视为例外。 (2认同)

nec*_*cer 16

例外之前,在C,有setjmplongjmp可用于完成栈帧的相似展开.

然后给同一个结构命名:"Exception".大多数答案都依赖于这个名称的含义来争论它的用法.它旨在用于特殊条件.这从未反映在原版中longjmp.在某些情况下,您需要在多个堆栈帧中打破控制流.

异常略微更通用,因为您也可以在同一堆栈框架中使用它们.这与goto我认为错误的类比提出了类比.Gotos是一个紧密耦合的对(也是如此)setjmplongjmpExceptions遵循松散耦合的发布/订阅更加清晰!

第三个混淆的来源是它们是被检查还是未经检查的例外.当然,未经检查的异常似乎特别难以用于控制流.

一旦你克服了维多利亚时代的所有挂断并且活了一点,那么检查的异常非常适合控制流程.

我最喜欢的用法是goto在一段长代码中的序列,它会一个接一个地尝试,直到找到它要查找的内容.每一件事 - 每一条逻辑 - 都可能有任意的嵌套,所以throw new Success()也可以进行任何条件测试.该break模式是脆.如果我if-else以其他方式编辑或弄乱语法,那么就会出现毛病.

使用else 线性化代码流.我使用本地定义的throw new Success()类 - 当然检查 - 所以如果我忘记捕获它,代码将无法编译.我没有抓住另一种方法Success.

有时我的代码会检查一个接一个的东西,只有在一切正常时才会成功.在这种情况下,我有一个类似的线性化使用Success.

使用单独的功能会混淆自然级别的区域化.所以throw new Failure()解决方案不是最优的.出于认知原因,我更喜欢在一个地方有一两页代码.我不相信超细分代码.

除非存在热点,否则JVM或编译器所做的事与我的关系不大.我无法相信编译器没有任何根本原因来检测本地抛出并捕获异常并简单地将它们视为return机器代码级别的高效s.

至于在控制流的功能中使用它们 - 即对于常见情况而不是特殊情况 - 我看不出它们如何效率低于多次中断,条件测试,返回通过三个堆栈帧而不仅仅是恢复堆栈指针.

我个人不会在堆栈帧中使用该模式,我可以看到如何优雅地设计复杂性.但是谨慎地使用它应该没问题.

最后,对于令人惊讶的处女程序员来说,这并不是一个令人信服的理由.如果你轻轻地将它们介绍给练习,他们就会学会喜欢它.我记得C++曾经惊讶并吓跑了C程序员.

  • 使用这种模式,我的大多数粗函数最后都有两个小的捕获 - 一个用于Success,一个用于Failure,这就是函数包含诸如准备正确的servlet响应或准备返回值之类的东西.有一个地方做总结是很好的.`return`-pattern替代方案需要为每个这样的函数提供两个函数.外部用于准备servlet响应或其他此类操作,而内部用于执行计算.PS:一位英国教授可能会建议我在最后一段中使用"惊人"而不是"令人惊讶":-) (3认同)

mou*_*iel 11

标准的anwser是异常不规则,应该在特殊情况下使用.

原因之一,这对我很重要,就是当我读了try-catch在我维护软件或调试控制结构,我试图找出原因,原来编码器使用的异常,而不是处理一个的if-else结构.我期待找到一个好的答案.

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


Jam*_*och 9

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


jas*_*hin 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;
}
Run Code Online (Sandbox Code Playgroud)

在任何一种情况下,您都需要检查以确保调用者正确使用您的API.但在第二种情况下,你需要它(隐含地).如果用户没有读取javadoc,仍然会抛出软异常,但是:

  1. 您不需要记录它.
  2. 您不需要对其进行测试(取决于您的单元测试策略的积极程度).
  3. 您不需要调用者处理三个用例.

地面点是异常应该用作返回代码,主要是因为你不仅复杂了你的API,还复杂了调用者的API.

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


paw*_*que 7

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

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

public Throwable fillInStackTrace() { return this; }
Run Code Online (Sandbox Code Playgroud)

这样的异常将没有填充堆栈跟踪.


Vla*_*mir 6

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

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


Jas*_*yon 5

我真的没有看到你如何控制你引用的代码中的程序流程.除了ArgumentOutOfRange异常之外,你永远不会看到另一个异常.(所以你的第二个catch子句永远不会被击中).你所做的只是使用极其昂贵的投掷来模仿if语句.

此外,你不会执行更加险恶的操作,你只是抛出一个异常,因为它被捕获到其他地方以执行流控制.你实际上是处理一个特例.


Sea*_*ean 5

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

例如,当我试图追踪 VS 中的错误时,我通常会打开“中断所有异常”。如果您使用异常进行流控制,那么我将定期中断调试器,并且将不得不继续忽略这些非异常异常,直到找到真正的问题为止。这很可能会让人抓狂!!