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

c#和Java(以及其他很多语言)有很多类型,有些“溢出”行为我一点也不喜欢(例如type。MaxValue +类型。SmallestValue ==类型。MinValue,例如int。MaxValue + 1 = int.MinValue)。

但是,鉴于我的邪恶本性,我将通过将此行为扩展为重写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结构更清晰,并且不会破坏方法所做的任何契约。

恕我直言,“永远不要在常规程序流程中使用它们”的反应似乎并不是每个人都有,因为这种反应的力量可以证明。

还是我说错了?

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

清晰的 尊重你的方法

拍我。


当前回答

在异常之前,C语言中有setjmp和longjmp可以用来完成类似的堆栈帧展开。

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

异常稍微更通用一些,因为您也可以在同一个堆栈框架中使用它们。我认为这与goto的类比是错误的。Gotos是紧密耦合的一对(setjmp和longjmp也是如此)。异常遵循松散耦合的发布/订阅,这要干净得多!因此,在相同的堆栈框架中使用它们与使用gotos几乎不是一回事。

混淆的第三个来源与它们是被检查的异常还是未检查的异常有关。当然,在控制流和许多其他事情上使用未检查异常似乎特别糟糕。

然而,受控异常对于控制流来说是很好的,一旦你克服了所有维多利亚式的困扰,并生活了一点。

我最喜欢的用法是在一长段代码中使用throw new Success()序列,它不断尝试一件事,直到找到它要找的东西。每一件事——每一段逻辑——都可能有任意的嵌套,因此break也会像任何类型的条件测试一样出现。如果-否则模式是脆弱的。如果我编辑了一个else,或者以其他方式弄乱了语法,那么就有一个毛茸茸的bug。

使用throw new Success()线性化代码流。我使用本地定义的成功类(当然是勾选的),这样如果我忘记捕获它,代码将无法编译。我没有捕捉到另一种方法的成功。

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

使用单独的函数会破坏划分的自然级别。所以返回解不是最优的。出于认知上的原因,我更喜欢在一个地方放一两页代码。我不相信超精细分割的代码。

除非有一个热点,否则jvm或编译器做什么对我来说不太重要。我不相信编译器有任何根本原因不检测本地抛出和捕获的异常,而只是在机器代码级别上将它们视为非常有效的goto。

至于在控制流的函数之间使用它们——即在常见情况下而不是特殊情况下——我不明白它们为什么会比多次中断、条件测试、返回来遍历三个堆栈帧而不是仅仅恢复堆栈指针的效率更低。

我个人并没有跨堆栈框架使用这种模式,我可以理解它需要复杂的设计才能做到这一点。但适当地使用它应该没问题。

最后,关于令人惊讶的新手程序员,这不是一个令人信服的理由。如果你温和地引导他们练习,他们就会爱上它。我记得c++曾经让C程序员感到惊讶和害怕。

其他回答

你可能会对Common Lisp的条件系统感兴趣,它是对异常的一种概括。因为你可以以一种可控的方式展开堆栈,也可以得到“重启”,这非常方便。

这与其他语言的最佳实践没有太大关系,但它向您展示了在您正在考虑的方向上,使用一些设计思想可以完成什么。

当然,如果您像溜溜球一样在堆栈上上下跳动,仍然存在性能考虑,但这比大多数捕获/抛出异常系统所体现的“哦,糟糕,让我们离开”这种方法要普遍得多。

我真的不明白你是如何在你引用的代码中控制程序流的。除了ArgumentOutOfRange异常,您将永远不会看到其他异常。(所以你的第二个catch条款永远不会被击中)。您所做的只是使用代价极高的抛出来模拟if语句。

此外,您也没有执行更危险的操作,即仅仅抛出一个异常,以便在其他地方捕获它以执行流控制。你实际上是在处理一个特例。

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

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

您是否曾经尝试调试一个在正常操作过程中每秒引发5个异常的程序?

我有。

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

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

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

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

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,软异常仍然会被抛出,但是:

你不需要记录它。 你不需要测试它(取决于你的攻击性有多强 单元测试策略是)。 您不需要调用者处理三个用例。

最基本的一点是,异常不应该用作返回代码,很大程度上是因为你不仅让你的API变得复杂了,还让调用者的API变得复杂了。

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