Why not use exceptions as regular flow of control?

后端 未结 24 2258
心在旅途
心在旅途 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: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.

提交回复
热议问题