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

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

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

还是我说错了?

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

清晰的 尊重你的方法

拍我。


当前回答

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

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

我记得在我学习C语言的时候,很多函数返回的错误代码是这样的:

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

etc.

它真的比抛出和捕获异常的可读性更差吗?

其他回答

但是你并不总是知道在你调用的方法中发生了什么。您将无法确切地知道异常抛出的位置。不需要更详细地检查异常对象....

很多答案的第一反应是:

你是在为程序员和最小惊讶原则写东西

当然!但如果只是一直不是更清楚。

这并不令人惊讶,例如:divide (1/x) catch (divisionByZero)对我(Conrad和其他人)来说比任何if都更清楚。事实上,这种编程是不被期待的,纯粹是传统的,实际上,仍然是相关的。也许在我的例子中,if会更清楚。

但是在这个问题上,DivisionByZero和FileNotFound比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分支。

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

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

但是,如果您知道哪里可能出错,而不使用if语句或其他检查方法,那么您就是懒惰。允许异常处理程序成为您知道可能发生的事情的所有人是懒惰的,它稍后会回来困扰您,因为您将试图根据可能错误的假设来修复异常处理程序中的情况。

如果您在异常处理程序中放入逻辑以确定究竟发生了什么,那么如果您没有将该逻辑放入try块中,那么您将非常愚蠢。

异常处理程序是最后的手段,当您无法阻止某些事情出错时,或者事情超出了您的控制能力。比如,服务器宕机并超时,而您无法阻止抛出异常。

最后,提前完成所有检查,显示您知道或预期会发生什么,并使其显式化。代码的意图应该清楚。你更愿意读什么?

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

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

然而,一些语言(尤其是Python)使用异常作为流控制结构。例如,如果没有其他项,迭代器将引发StopIteration异常。即使是标准的语言结构(比如for)也依赖于此。