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

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结构更清晰,并且不会破坏方法所做的任何契约。

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

还是我说错了?

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

清晰的 尊重你的方法

拍我。


当前回答

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

如果您以一种意想不到的方式使用特性,同时又选择不使用为此目的而设计的特性,那么将会有相关的成本。在这种情况下,清晰度和性能没有真正的附加价值。与广泛接受的if语句相比,使用异常能给您带来什么?

换句话说:你能做并不意味着你应该做。

其他回答

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

在JVM中,抛出异常的代价并不高,只需使用新的xyzException(…)创建异常,因为后者涉及堆栈遍历。因此,如果您预先创建了一些异常,您可以多次抛出它们而无需付出任何代价。当然,这种方式不能将数据与异常一起传递,但我认为无论如何这都是一件糟糕的事情。

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

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

正如其他人已经多次提到的,最小惊讶原则将禁止您仅为控制流的目的而过度使用异常。另一方面,没有任何规则是100%正确的,总有一些情况下,异常是“合适的工具”——就像goto本身,顺便说一下,它在Java等语言中以break和continue的形式发布,这通常是跳出大量嵌套循环的完美方式,而这种循环并不总是可以避免的。

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

http://blog.jooq.org/2013/04/28/rare-uses-of-a-controlflowexception

它解释了在jOOQ (Java的SQL抽象库)内部,当满足某些“罕见”条件时,如何偶尔使用这种异常来提前中止SQL呈现过程。

这种条件的例子有:

Too many bind values are encountered. Some databases do not support arbitrary numbers of bind values in their SQL statements (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). In those cases, jOOQ aborts the SQL rendering phase and re-renders the SQL statement with inlined bind values. Example: // 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(); } If we explicitly extracted the bind values from the query AST to count them every time, we'd waste valuable CPU cycles for those 99.9% of the queries that don't suffer from this problem. Some logic is available only indirectly via an API that we want to execute only "partially". The UpdatableRecord.store() method generates an INSERT or UPDATE statement, depending on the Record's internal flags. From the "outside", we don't know what kind of logic is contained in store() (e.g. optimistic locking, event listener handling, etc.) so we don't want to repeat that logic when we store several records in a batch statement, where we'd like to have store() only generate the SQL statement, not actually execute it. Example: // 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()); } } If we had externalised the store() logic into "re-usable" API that can be customised to optionally not execute the SQL, we'd be looking into creating a rather hard to maintain, hardly re-usable API.

结论

从本质上讲,我们对这些非本地goto的使用就像[Mason Wheeler][5]在他的回答中所说的那样:

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

controlflowexception的两种用法与它们的替代方法相比都很容易实现,允许我们重用广泛的逻辑,而无需从相关的内部重构它。

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

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

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

因为代码很难阅读,你可能在调试它时遇到麻烦,在很长一段时间后修复错误时,你会引入新的错误,在资源和时间方面更昂贵,如果你正在调试代码,调试器在每次异常发生时停止,这让你很恼火;)