为什么不把exception用作正常的控制stream?

为了避免所有的标准答案,我可以谷歌search,我会提供一个你可以随意攻击的例子。

C#和Java(以及其他)有很多types的溢出行为,我不喜欢所有types(例如type.MaxValue + type.SmallestValue == type.MinValue ,例如: int.MaxValue + 1 == int.MinValue )。

但是,看到我的恶毒天性,我会通过扩展这种行为来增加一些侮辱,比如重写DateTimetypes。 (我知道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. } } 

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

我认为在很多情况下,他们比if结构更清楚,并且不会违反方法制定的任何合同。

恕我直言,“从来没有使用他们的正常程序stream”反应每个人似乎都没有那么好的underbuild作为反应的力量可以certificate。

还是我误会了?

我已经阅读过其他文章,处理各种特殊情况,但是我的观点是如果你们两个都没有错,

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

拍我

你有没有试过在正常的操作过程中debugging一个程序,每秒产生五次exception?

我有。

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

我希望我能启动程序并等待exception发生,但是在正常的操作过程中,在启动过程中有大约200个exception

我的观点: 如果你在正常情况下使用exception,你如何findexception情况?

当然,还有其他强有力的理由,不要太多地使用例外,特别是在性能方面

例外情况基本上是非本地的goto声明,后者的所有后果。 使用exception进行stream量控制违反了最不惊讶的原则 ,使程序难以阅读(请记住,程序员首先为程序员编写程序)。

而且,这不是编译器厂商所期望的。 他们希望很less抛出exception,而且通常会让throw代码非常低效。 抛出exception是.NET中最昂贵的操作之一。

但是,一些语言(特别是Python)使用exception作为stream程控制结构。 例如,如果没有其他项目,迭代器将引发StopIterationexception。 即使是标准的语言结构(如for )也依赖于此。

对很多答案的第一反应是:

你正在为程序员和最不惊讶的原则写信

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

它不应该是惊人的,例如:除(1 / x)catch(divisionByZero)比任何对我(在Conrad和其他人)更清楚。 事实上,这种程序devise并不是纯粹的传统,事实上也是相关的。 也许在我的例子中,如果会更清楚。

但是,DivisionByZero和FileNotFound比这个更清晰。

当然,如果性能不高并且每秒钟需要数十亿次,那么当然应该避免这个问题,但是我还没有看到任何避免整体devise的理由。

就最简单的原则而言:这里有一个循环推理的危险:假设整个社区使用了一个糟糕的devise,这个devise将会变成预期! 因此,这个原则不可能是一个大杯,应该小心翼翼地进行。

正常情况下的例外,你如何find不寻常的(例外)情况?

在许多反应中 像这样发光低谷。 抓住他们,不是? 你的方法应该是清楚的,有据可查的,并且是合同。 我不知道我必须承认这个问题。

在所有exception情况下进行debugging:同样的,有时候这样做是因为devise不使用exception是很常见的。 我的问题是:为什么一开始就很普遍?

我的经验法则是:

  • 如果你可以做任何事情从错误中恢复,赶上例外
  • 如果错误是非常常见的错误(例如,用户尝试使用错误的密码login),请使用返回值
  • 如果你不能做任何事情从错误中恢复,把它放在一边(或者抓住你的主要捕手来做一些半正常closures的应用程序)

我看到exception的问题是从纯粹的语法angular度(我敢肯定,性能开销是最小的)。 我不喜欢尝遍各地。

以这个例子:

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

..另一个例子可能是当你需要使用一个工厂分配一个句柄给一个句柄,那个工厂可能会抛出一个exception:

 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.. :( 

所以,我个人认为,应该为罕见的错误条件(内存不足等)保留exception,并使用返回值(值类,结构或枚举)来进行错误检查。

希望我明白你的问题是正确的:)

在例外之前,在C中,有setjmplongjmp可以用来完成堆栈框架的类似展开。

然后同样的结构被赋予一个名称:“例外”。 而大部分的答案依赖于这个名字的含义来争论它的用法。 它旨在用于特殊的条件。 这从来没有反映在原来的longjmp 。 有些情况下你需要打破许多堆栈帧的控制stream。

例外情况稍微更一般,因为您也可以在同一个堆栈框架中使用它们。 这引起了与goto类比,我相信是错误的。 Gotos是一个紧密耦合的对( setjmplongjmp也是如此)exception遵循一个松散耦合的发布/订阅,是非常干净的!

混淆的第三个来源与它们是否被检查或未被检查的例外有关。 当然,未经检查的exception对控制stream程来说似乎特别糟糕。

一旦你克服了所有的维多利亚时代的睡眠并且活一点,检查exception对于控制stream程来说是非常好的。

我最喜欢的用法是在一段很长的代码段中throw new Success() ,这个代码片段试图一个接一个,直到find它正在寻找的东西。 每一件事情 – 每一个逻辑 – 都可能有一个任意的嵌套,所以break和其他任何一种状态testing都是一样的。 if-else模式很脆弱。 如果我编辑一个else或者以其他方式搞乱了语法,那么就有一个毛茸茸的bug。

使用throw new Success()使代码stream线性化 。 我使用本地定义的Success类 – 当然检查 – 所以,如果我忘了赶上它的代码将不会编译。 而且我没有接受另一种方法的Success

有时我的代码会一个接一个地检查一件事情,只有一切正常才能成功。 在这种情况下,我有一个类似的线性化使用throw new Failure()

使用一个单独的函数混乱自然水平的条块分割。 所以return解决scheme不是最优的。 出于认知的原因,我宁愿在一个地方有一两页的代码。 我不相信超精细的代码。

除非有热点,否则JVM或编译器与我的关系不大。 我不能相信编译器没有任何根本原因检测本地抛出和捕获exception,并简单地把它们视为在机器代码级别非常有效的转移。

至于在控制stream程中跨越函数使用它们,也就是说,对于常见情况而不是例外情况,我不能看到它们如何比多个中断,条件testing,通过三个堆栈框架返回而不是仅仅恢复堆栈指针。

我个人不使用堆栈框架中的模式,我可以看到它是如何优雅地devise复杂性。 但谨慎使用,应该没问题。

最后,关于令人惊讶的处女程序员,这不是一个令人信服的理由。 如果你轻轻地介绍他们的做法,他们会学会去爱。 我记得C ++曾经让C程序员感到惊讶和恐惧。

标准的规定是例外情况不规则,应在特殊情况下使用。

其中一个重要的原因是,当我在维护或debugging的软件中读取try-catch控制结构时,我试图找出原始编码器为什么使用exception处理而不是if-else结构。 我希望find一个好的答案。

请记住,您不仅要为计算机编写代码,还要为其他编码器编写代码。 有一个与exception处理程序相关联的语义,因为机器不介意就不能丢弃。

性能如何? 在.NET Web应用程序的加载testing中,我们以每个Web服务器100个模拟用户为榜样,直到我们修复了一个常见的例外,并且该数量增加到了500个用户。

Josh Bloch在Effective Java中广泛地讨论了这个话题。 他的build议是明智的,也应该适用于.Net(除了细节)。

特殊情况下应该使用例外情况。 其原因主要是可用性相关。 对于一个给定的方法是最大可用的,其input和输出条件应该是最大限度的约束。

例如,第二种方法比第一种方法更容易使用:

 /** * 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,则软件exception仍然会被抛出,但是:

  1. 你不需要logging它。
  2. 你不需要testing它(取决于你的unit testing策略是如何攻击的)。
  3. 你不需要调用者来处理三个用例。

基本的观点是Exceptions不应该被用作返回代码,主要是因为你不仅复杂了你的API,而且还复杂了调用者的API。

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

我认为你可以使用例外进行stream量控制。 然而,这种技术有一个缺点。 创buildexception是一件昂贵的事情,因为他们必须创build一个堆栈跟踪。 所以,如果你想更频繁地使用exception,而不是仅仅发出exception情况,那么你必须确保堆栈跟踪不会对性能产生负面影响。

减less创buildexception成本的最好方法是重写fillInStackTrace()方法,如下所示:

 public Throwable fillInStackTrace() { return this; } 

这样的例外将没有填写的堆栈轨迹。

我真的不知道你在引用的代码中是如何控制程序stream的。 除了ArgumentOutOfRangeexception之外,您永远不会看到另一个exception。 (所以你的第二个catch子句将永远不会被击中)。 所有你正在做的是用一个非常昂贵的投掷来模仿一个if语句。

另外,你不是在执行更险恶的操作,只是抛出一个exception,而只是为了让它在其他地方被抓到来执行stream量控制。 你实际上正在处理一个特例。

由于代码难以阅读,因此您可能在debugging时遇到麻烦,在长时间修复错误时会引入新的错误,在资源和时间方面会更加昂贵,如果您正在debugging代码,debugging器停止发生每个exception;)

除了上述原因之外,不使用stream量控制exception的一个原因是它可能使debugging过程变得非常复杂。

例如,当我试图追踪VS中的一个错误时,我通常会打开“打破所有exception”。 如果你使用exception进行stream量控制,那么我将定期在debugging器中进行debugging,并且必须保持忽略这些非例外的exception,直到遇到真正的问题。 这可能会导致某人疯狂!

您可以使用锤子的爪来转动螺丝,就像您可以使用控制stream程的例外一样。 这并不意味着它是该function的预期用法if语句表示条件,其用途控制stream。

如果您在select不使用为此目的而devise的function的同时以无意的方式使用function,则会有相关的成本。 在这种情况下,清晰度和性能受到影响,没有真正的附加价值。 什么使用例外购买你在广泛接受的if语句?

换句话说:只因为你可以不意味着你应该

让我们假设你有一个方法来做一些计算。 有许多input参数需要validation,然后返回一个大于0的数字。

使用返回值来表示validation错误很简单:如果方法返回一个小于0的数字,则发生错误。 如何知道哪个参数没有被validation?

我记得我的C天很多函数返回错误代码,如下所示:

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

等等

抛出和捕获exception真的不太可读吗?

但是你不会总是知道你所调用的方法会发生什么。 你将不知道exception抛出的位置。 没有更详细地检查exception对象….

通常,在低级别处理例外情况本身没有任何问题。 一个exception是一个有效的消息,它提供了很多为什么无法执行操作的细节。 如果你能处理它,你应该。

一般来说,如果你知道有很高的失败可能性,你可以检查…你应该做的检查…即如果(obj!= null)obj.method()

在你的情况下,我不熟悉C#库知道date时间是否有一个简单的方法来检查时间戳是否超出范围。 如果是这样,只要调用if(.isvalid(ts))否则你的代码基本上是好的。

所以,基本上这取决于哪种方式创build更干净的代码…如果预防exception的操作比处理exception更复杂, 比你有我的许可来处理例外,而不是在任何地方build立复杂的守卫。

如果你正在使用exception处理程序来控制stream程,那你就太普通而懒惰了。 正如别人提到的,如果你正在处理处理程序中的处理,你知道发生了什么事情,但究竟是什么? 如果你正在使用它作为控制stream,基本上你正在使用else语句的exception。

如果您不知道可能发生什么状态,那么您可以使用exception处理程序来处理意外状态,例如,当您必须使用第三方库时,或者必须捕捉UI中的所有内容才能显示出错误消息并loggingexception。

但是,如果你确实知道什么可能会出错,而且你没有写一个if语句或什么来检查它,那么你只是懒惰。 允许exception处理程序成为你可能发生的事情的一部分是懒惰的,它会在以后回来困扰你,因为你将尝试根据可能的错误假设来修正exception处理程序中的情况。

如果你在你的exception处理程序中放置逻辑来确定究竟发生了什么,那么你不会把这个逻辑放在try块内。

例外处理程序是最后的手段,因为当你用完想法/方法来阻止某些事情发生错误,或者事情超出了你的控制能力。 就像服务器closures并超时一样,你不能防止抛出exception。

最后,完成所有的检查显示你知道或期望会发生什么,并明确表示。 代码应该明确的意图。 你想读什么?

您可能有兴趣查看一下Common Lisp的条件系统,这是对exception进行正确的归纳。 因为你可以放松堆叠或者没有受控的方式,所以你也可以“重新启动”,这非常方便。

这与其他语言的最佳实践没有太大的关系,但是它向您展示了(大致)您正在考虑的方向上的某些devise思想可以做些什么。

Of course there are still performance considerations if you're bouncing up and down the stack like a yo-yo, but it's a much more general idea than "oh crap, lets bail" kind of approach that most catch/throw exception systems embody.

I don't think there is anything wrong with using Exceptions for flow-control. Exceptions are somewhat similar to continuations and in statically typed languages, Exceptions are more powerful than continuations, so, if you need continuations but your language doesn't have them, you can use Exceptions to implement them.

Well, actually, if you need continuations and your language doesn't have them, you chose the wrong language and you should rather be using a different one. But sometimes you don't have a choice: client-side web programming is the prime example – there's just no way to get around JavaScript.

An example: Microsoft Volta is a project to allow writing web applications in straight-forward .NET, and let the framework take care of figuring out which bits need to run where. One consequence of this is that Volta needs to be able to compile CIL to JavaScript, so that you can run code on the client. However, there is a problem: .NET has multithreading, JavaScript doesn't. So, Volta implements continuations in JavaScript using JavaScript Exceptions, then implements .NET Threads using those continuations. That way, Volta applications that use threads can be compiled to run in an unmodified browser – no Silverlight needed.

One aesthetic reason:

A try always comes with a catch, whereas an if doesn't have to come with an else.

 if (PerformCheckSucceeded()) DoSomething(); 

With try/catch, it becomes much more verbose.

 try { PerformCheckSucceeded(); DoSomething(); } catch { } 

That's 6 lines of code too many.

As others have mentioned numerously, the principle of least astonishment will forbid that you use exceptions excessively for control flow only purposes. On the other hand, no rule is 100% correct, and there are always those cases where an exception is "just the right tool" – much like goto itself, by the way, which ships in the form of break and continue in languages like Java, which are often the perfect way to jump out of heavily nested loops, which aren't always avoidable.

The following blog post explains a rather complex but also rather interesting use-case for a non-local ControlFlowException :

It explains how inside of jOOQ (a SQL abstraction library for Java) , such exceptions are occasionally used to abort the SQL rendering process early when some "rare" condition is met.

Examples of such conditions are:

  • 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. 例:

     // 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() (eg 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. 例:

     // 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.

结论

In essence, our usage of these non-local goto s is just along the lines of what [Mason Wheeler][5] said in his answer:

"I just encountered a situation that I cannot deal with properly at this point, because I don't have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it."

Both usages of ControlFlowExceptions were rather easy to implement compared to their alternatives, allowing us to reuse a wide range of logic without refactoring it out of the relevant internals.

But the feeling of this being a bit of a surprise to future maintainers remains. The code feels rather delicate and while it was the right choice in this case, we'd always prefer not to use exceptions for local control flow, where it is easy to avoid using ordinary branching through if - else .

Here are best practices I described in my blog post :

  • Throw an exception to state an unexpected situation in your software.
  • Use return values for input validation .
  • If you know how to deal with exceptions a library throws, catch them at the lowest level possible .
  • If you have an unexpected exception, discard current operation completely. Don't pretend you know how to deal with them .

I feel that there is nothing wrong with your example. On the contrary, it would be a sin to ignore the exception thrown by the called function.

In the JVM, throwing an exception is not that expensive, only creating the exception with new xyzException(…), because the latter involves a stack walk. So if you have some exceptions created in advance, you may throw them many times without costs. Of course, this way you can't pass data along with the exception, but I think that is a bad thing to do anyway.

There are a few general mechanisms via which a language could allow for a method to exit without returning a value and unwind to the next "catch" block:

  • Have the method examine the stack frame to determine the call site, and use the metadata for the call site to find either information about a try block within the calling method, or the location where the calling method stored the address of its caller; in the latter situation, examine metadata for the caller's caller to determine in the same fashion as the immediate caller, repeating until one finds a try block or the stack is empty. This approach adds very little overhead to the no-exception case (it does preclude some optimizations) but is expensive when an exception occurs.

  • Have the method return a "hidden" flag which distinguishes a normal return from an exception, and have the caller check that flag and branch to an "exception" routine if it's set. This routine adds 1-2 instructions to the no-exception case, but relatively little overhead when an exception occurs.

  • Have the caller place exception-handling information or code at a fixed address relative to the stacked return address. For example, with the ARM, instead of using the instruction "BL subroutine", one could use the sequence:

      adr lr,next_instr b subroutine b handle_exception next_instr: 

To exit normally, the subroutine would simply do bx lr or pop {pc} ; in case of an abnormal exit, the subroutine would either subtract 4 from LR before performing the return or use sub lr,#4,pc (depending upon the ARM variation, execution mode, etc.) This approach will malfunction very badly if the caller is not designed to accommodate it.

A language or framework which uses checked exceptions might benefit from having those handled with a mechanism like #2 or #3 above, while unchecked exceptions are handled using #1. Although the implementation of checked exceptions in Java is rather nuisancesome, they would not be a bad concept if there were a means by which a call site could say, essentially, "This method is declared as throwing XX, but I don't expect it ever to do so; if it does, rethrow as an "unchecked" exception. In a framework where checked exceptions were handled in such fashion, they could be an effective means of flow control for things like parsing methods which in some contexts may have a high likelihood of failure, but where failure should return fundamentally different information than success. I'm unaware of any frameworks that use such a pattern, however. Instead, the more common pattern is to use the first approach above (minimal cost for the no-exception case, but high cost when exceptions are thrown) for all exceptions.