Why not use exceptions as regular flow of control?

后端 未结 24 1851
心在旅途
心在旅途 2020-11-21 07:30

To avoid all standard-answers I could have Googled on, I will provide an example you all can attack at will.

C# and Java (and too many others) have with plenty of ty

相关标签:
24条回答
  • 2020-11-21 08:14

    Apart from the reasons stated, one reason not to use exceptions for flow control is that it can greatly complicate the debugging process.

    For example, when I'm trying to track down a bug in VS I'll typically turn on "break on all exceptions". If you're using exceptions for flow control then I'm going to be breaking in the debugger on a regular basis and will have to keep ignoring these non-exceptional exceptions until I get to the real problem. This is likely to drive someone mad!!

    0 讨论(0)
  • 2020-11-21 08:16

    If you are using exception handlers for control flow, you are being too general and lazy. As someone else mentioned, you know something happened if you are handling processing in the handler, but what exactly? Essentially you are using the exception for an else statement, if you are using it for control flow.

    If you don't know what possible state could occur, then you can use an exception handler for unexpected states, for example when you have to use a third-party library, or you have to catch everything in the UI to show a nice error message and log the exception.

    However, if you do know what might go wrong, and you don't put an if statement or something to check for it, then you are just being lazy. Allowing the exception handler to be the catch-all for stuff you know could happen is lazy, and it will come back to haunt you later, because you will be trying to fix a situation in your exception handler based on a possibly false assumption.

    If you put logic in your exception handler to determine what exactly happened, then you would be quite stupid for not putting that logic inside the try block.

    Exception handlers are the last resort, for when you run out of ideas/ways to stop something from going wrong, or things are beyond your ability to control. Like, the server is down and times out and you can't prevent that exception from being thrown.

    Finally, having all the checks done up front shows what you know or expect will occur and makes it explicit. Code should be clear in intent. What would you rather read?

    0 讨论(0)
  • 2020-11-21 08:17

    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:

    • http://blog.jooq.org/2013/04/28/rare-uses-of-a-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. 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.

    Conclusion

    In essence, our usage of these non-local gotos 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.

    0 讨论(0)
  • 2020-11-21 08:18

    Before exceptions, in C, there were setjmp and longjmp that could be used to accomplish a similar unrolling of the stack frame.

    Then the same construct was given a name: "Exception". And most of the answers rely on the meaning of this name to argue about its usage, claiming that exceptions are intended to be used in exceptional conditions. That was never the intent in the original longjmp. There were just situations where you needed to break control flow across many stack frames.

    Exceptions are slightly more general in that you can use them within the same stack frame too. This raises analogies with goto that I believe are wrong. Gotos are a tightly coupled pair (and so are setjmp and longjmp). Exceptions follow a loosely coupled publish/subscribe that is much cleaner! Therefore using them within the same stack frame is hardly the same thing as using gotos.

    The third source of confusion relates to whether they are checked or unchecked exceptions. Of course, unchecked exceptions seem particularly awful to use for control flow and perhaps a lot of other things.

    Checked exceptions however are great for control flow, once you get over all the Victorian hangups and live a little.

    My favorite usage is a sequence of throw new Success() in a long fragment of code that tries one thing after the other until it finds what it is looking for. Each thing -- each piece of logic -- may have arbritrary nesting so break's are out as also any kind of condition tests. The if-else pattern is brittle. If I edit out an else or mess up the syntax in some other way, then there is a hairy bug.

    Using throw new Success() linearizes the code flow. I use locally defined Success classes -- checked of course -- so that if I forget to catch it the code won't compile. And I don't catch another method's Successes.

    Sometimes my code checks for one thing after the other and only succeeds if everything is OK. In this case I have a similar linearization using throw new Failure().

    Using a separate function messes with the natural level of compartmentalization. So the return solution is not optimal. I prefer to have a page or two of code in one place for cognitive reasons. I don't believe in ultra-finely divided code.

    What JVMs or compilers do is less relevant to me unless there is a hotspot. I cannot believe there is any fundamental reason for compilers to not detect locally thrown and caught Exceptions and simply treat them as very efficient gotos at the machine code level.

    As far as using them across functions for control flow -- i. e. for common cases rather than exceptional ones -- I cannot see how they would be less efficient than multiple break, condition tests, returns to wade through three stack frames as opposed to just restore the stack pointer.

    I personally do not use the pattern across stack frames and I can see how it would require design sophistication to do so elegantly. But used sparingly it should be fine.

    Lastly, regarding surprising virgin programmers, it is not a compelling reason. If you gently introduce them to the practice, they will learn to love it. I remember C++ used to surprise and scare the heck out of C programmers.

    0 讨论(0)
  • 2020-11-21 08:21

    My rule of thumb is:

    • If you can do anything to recover from an error, catch exceptions
    • If the error is a very common one (eg. user tried to log in with the wrong password), use returnvalues
    • If you can't do anything to recover from an error, leave it uncaught (Or catch it in your main-catcher to do some semi-graceful shutdown of the application)

    The problem I see with exceptions is from a purely syntax point of view (I'm pretty sure the perfomance overhead is minimal). I don't like try-blocks all over the place.

    Take this example:

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

    .. Another example could be when you need to assign something to a handle using a factory, and that factory could throw an 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.. :(
    

    Soo, personally, I think you should keep exceptions for rare error-conditions (out of memory etc.) and use returnvalues (valueclasses, structs or enums) to do your error checking instead.

    Hope I understood your question correct :)

    0 讨论(0)
  • 2020-11-21 08:21

    A first reaction to a lot of answers :

    you're writing for the programmers and the principle of least astonishment

    Of course! But an if just isnot more clear all the time.

    It shouldn't be astonishing eg : divide (1/x) catch (divisionByZero) is more clear than any if to me (at Conrad and others) . The fact this kind of programming isn't expected is purely conventional, and indeed, still relevant. Maybe in my example an if would be clearer.

    But DivisionByZero and FileNotFound for that matter are clearer than ifs.

    Of course if it's less performant and needed a zillion time per sec, you should of course avoid it, but still i haven't read any good reason to avoid the overal design.

    As far as the principle of least astonishment goes : there's a danger of circular reasoning here : suppose a whole community uses a bad design, this design will become expected! Therefore the principle cannot be a grail and should be concidered carefully.

    exceptions for normal situations, how do you locate unusual (ie exceptional) situations ?

    In many reactions sth. like this shines trough. Just catch them, no? Your method should be clear, well documented, and hounouring it's contract. I don't get that question I must admit.

    Debugging on all exceptions : the same, that's just done sometimes because the design not to use exceptions is common. My question was : why is it common in the first place?

    0 讨论(0)
提交回复
热议问题