Pattern to avoid nested try catch blocks?

后端 未结 16 511
無奈伤痛
無奈伤痛 2020-12-12 12:53

Consider a situation where I have three (or more) ways of performing a calculation, each of which can fail with an exception. In order to attempt each calculation until we f

相关标签:
16条回答
  • 2020-12-12 13:22

    You can use a Task/ContinueWith, and check for the exception. Here's a nice extension method to help make it pretty:

        static void Main() {
            var task = Task<double>.Factory.StartNew(Calc1)
                .OrIfException(Calc2)
                .OrIfException(Calc3)
                .OrIfException(Calc4);
            Console.WriteLine(task.Result); // shows "3" (the first one that passed)
        }
    
        static double Calc1() {
            throw new InvalidOperationException();
        }
    
        static double Calc2() {
            throw new InvalidOperationException();
        }
    
        static double Calc3() {
            return 3;
        }
    
        static double Calc4() {
            return 4;
        }
    }
    
    static class A {
        public static Task<T> OrIfException<T>(this Task<T> task, Func<T> nextOption) {
            return task.ContinueWith(t => t.Exception == null ? t.Result : nextOption(), TaskContinuationOptions.ExecuteSynchronously);
        }
    }
    
    0 讨论(0)
  • 2020-12-12 13:24

    As far as possible, don't use exceptions for control flow or unexceptional circumstances.

    But to answer your question directly (assuming all the exception-types are the same):

    Func<double>[] calcs = { calc1, calc2, calc3 };
    
    foreach(var calc in calcs)
    {
       try { return calc(); }
       catch (CalcException){  }
    } 
    
    throw new NoCalcsWorkedException();
    
    0 讨论(0)
  • 2020-12-12 13:26

    If the actual type of the exception thrown doesn't matter, you can just use a typeless catch block:

    var setters = new[] { calc1, calc2, calc3 };
    bool succeeded = false;
    foreach(var s in setters)
    {
        try
        {
                val = s();
                succeeded = true;
                break;
        }
        catch { /* continue */ }
    }
    if (!suceeded) throw new NoCalcsWorkedException();
    
    0 讨论(0)
  • 2020-12-12 13:27
    using System;
    
    namespace Utility
    {
        /// <summary>
        /// A helper class for try-catch-related functionality
        /// </summary>
        public static class TryHelper
        {
            /// <summary>
            /// Runs each function in sequence until one throws no exceptions;
            /// if every provided function fails, the exception thrown by
            /// the final one is left unhandled
            /// </summary>
            public static void TryUntilSuccessful( params Action[] functions )
            {
                Exception exception = null;
    
                foreach( Action function in functions )
                {
                    try
                    {
                        function();
                        return;
                    }
                    catch( Exception e )
                    {
                        exception   = e;
                    }
                }
    
                throw exception;
            }
        }
    }
    

    And use it like so:

    using Utility;
    
    ...
    
    TryHelper.TryUntilSuccessful(
        () =>
        {
            /* some code */
        },
        () =>
        {
            /* more code */
        },
        calc1,
        calc2,
        calc3,
        () =>
        {
            throw NotImplementedException();
        },
        ...
    );
    
    0 讨论(0)
  • 2020-12-12 13:28

    You could flatten out the nesting by putting it into a method like this:

    private double calcStuff()
    {
      try { return calc1(); }
      catch (Calc1Exception e1)
      {
        // Continue on to the code below
      }
    
      try { return calc2(); }
      catch (Calc2Exception e1)
      {
        // Continue on to the code below
      }
    
      try { return calc3(); }
      catch (Calc3Exception e1)
      {
        // Continue on to the code below
      }
    
      throw new NoCalcsWorkedException();
    }
    

    But I suspect the real design problem is the existence of three different methods that do essentially the same thing (from the caller's perspective) but throw different, unrelated exceptions.

    This is assuming the three exceptions are unrelated. If they all have a common base class, it'd be better to use a loop with a single catch block, as Ani suggested.

    0 讨论(0)
  • 2020-12-12 13:30

    You are right about wrapping each calculation but you should wrap according to the tell-don't-ask-principle.

    double calc3WithConvertedException(){
        try { val = calc3(); }
        catch (Calc3Exception e3)
        {
            throw new NoCalcsWorkedException();
        }
    }
    
    double calc2DefaultingToCalc3WithConvertedException(){
        try { val = calc2(); }
        catch (Calc2Exception e2)
        {
            //defaulting to simpler method
            return calc3WithConvertedException();
        }
    }
    
    
    double calc1DefaultingToCalc2(){
        try { val = calc2(); }
        catch (Calc1Exception e1)
        {
            //defaulting to simpler method
            return calc2defaultingToCalc3WithConvertedException();
        }
    }
    

    The operations are simple, and can change their behaviour independently. And it doesn't matter why they default. As a prove you could implement calc1DefaultingToCalc2 as:

    double calc1DefaultingToCalc2(){
        try { 
            val = calc2(); 
            if(specialValue(val)){
                val = calc2DefaultingToCalc3WithConvertedException()
            }
        }
        catch (Calc1Exception e1)
        {
            //defaulting to simpler method
            return calc2defaultingToCalc3WithConvertedException();
        }
    }
    
    0 讨论(0)
提交回复
热议问题