Pattern to avoid nested try catch blocks?

后端 未结 16 509
無奈伤痛
無奈伤痛 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:15

    Try not to control logic based on exceptions; note also that exceptions should be thrown only in exceptional cases. Calculations in most cases should not throw exceptions unless they access external resources or parse strings or something. Anyway in the worst case follow the TryMethod style (like TryParse()) to encapsulate exception logic and make your control flow maintainable and clean:

    bool TryCalculate(out double paramOut)
    {
      try
      {
        // do some calculations
        return true;
      }
      catch(Exception e)
      { 
         // do some handling
        return false;
      }
    
    }
    
    double calcOutput;
    if(!TryCalc1(inputParam, out calcOutput))
      TryCalc2(inputParam, out calcOutput);
    

    Another variation utilizing the Try pattern and combining list of methods instead of nested if:

    internal delegate bool TryCalculation(out double output);
    
    TryCalculation[] tryCalcs = { calc1, calc2, calc3 };
    
    double calcOutput;
    foreach (var tryCalc in tryCalcs.Where(tryCalc => tryCalc(out calcOutput)))
      break;
    

    and if the foreach is a little complicated you can make it plain:

            foreach (var tryCalc in tryCalcs)
            {
                if (tryCalc(out calcOutput)) break;
            }
    
    0 讨论(0)
  • It seems that the OP's intention was to find a good pattern for solving his issue and resolving the current problem that he was struggling with at that moment.

    OP: "I could wrap each calculation in a helper method which returns null on failure, and then just use the ?? operator, but is there a way of doing this more generally (i.e. without having to write a helper method for each method I want to use)? I've thought about writing a static method using generics which wraps any given method in a try/catch and returns null on failure, but I'm not sure how I would go about this. Any ideas?"

    I saw a lot of good patterns that avoid nested try catch blocks, posted in this feed, but didn't find a solution to the problem that is cited above. So, here is the solution:

    As OP mentioned above, he wanted to make a wrapper object which returns null on failure. I would call it a pod (Exception-safe pod).

    public static void Run()
    {
        // The general case
        // var safePod1 = SafePod.CreateForValueTypeResult(() => CalcX(5, "abc", obj));
        // var safePod2 = SafePod.CreateForValueTypeResult(() => CalcY("abc", obj));
        // var safePod3 = SafePod.CreateForValueTypeResult(() => CalcZ());
    
        // If you have parameterless functions/methods, you could simplify it to:
        var safePod1 = SafePod.CreateForValueTypeResult(Calc1);
        var safePod2 = SafePod.CreateForValueTypeResult(Calc2);
        var safePod3 = SafePod.CreateForValueTypeResult(Calc3);
    
        var w = safePod1() ??
                safePod2() ??
                safePod3() ??
                throw new NoCalcsWorkedException(); // I've tested it on C# 7.2
    
        Console.Out.WriteLine($"result = {w}"); // w = 2.000001
    }
    
    private static double Calc1() => throw new Exception("Intentionally thrown exception");
    private static double Calc2() => 2.000001;
    private static double Calc3() => 3.000001;
    

    But what if you'd like to create a safe pod for a Reference Type result returned by CalcN() functions/methods.

    public static void Run()
    {
        var safePod1 = SafePod.CreateForReferenceTypeResult(Calc1);
        var safePod2 = SafePod.CreateForReferenceTypeResult(Calc2);
        var safePod3 = SafePod.CreateForReferenceTypeResult(Calc3);
    
        User w = safePod1() ?? safePod2() ?? safePod3();
    
        if (w == null) throw new NoCalcsWorkedException();
    
        Console.Out.WriteLine($"The user object is {{{w}}}"); // The user object is {Name: Mike}
    }
    
    private static User Calc1() => throw new Exception("Intentionally thrown exception");
    private static User Calc2() => new User { Name = "Mike" };
    private static User Calc3() => new User { Name = "Alex" };
    
    class User
    {
        public string Name { get; set; }
        public override string ToString() => $"{nameof(Name)}: {Name}";
    }
    

    So, you might notice that there is no need "to write a helper method for each method you want to use".

    The two types of pods (for ValueTypeResults and ReferenceTypeResults) are enough.


    Here is the code of SafePod. It isn't a container though. Instead, it creates an exception-safe delegate wrapper for both ValueTypeResults and ReferenceTypeResults.

    public static class SafePod
    {
        public static Func<TResult?> CreateForValueTypeResult<TResult>(Func<TResult> jobUnit) where TResult : struct
        {
            Func<TResult?> wrapperFunc = () =>
            {
                try { return jobUnit.Invoke(); } catch { return null; }
            };
    
            return wrapperFunc;
        }
    
        public static Func<TResult> CreateForReferenceTypeResult<TResult>(Func<TResult> jobUnit) where TResult : class
        {
            Func<TResult> wrapperFunc = () =>
            {
                try { return jobUnit.Invoke(); } catch { return null; }
            };
    
            return wrapperFunc;
        }
    }
    

    That's how you can leverage the null-coalescing operator ?? combined with the power of first-class citizen entities (delegates).

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

    In Perl you can do foo() or bar(), which will execute bar() if foo() fails. In C# we don't see this "if fail, then" construct, but there's an operator that we can use for this purpose: the null-coalesce operator ??, which continues only if the first part is null.

    If you can change the signature of your calculations and if you either wrap their exceptions (as shown in previous posts) or rewrite them to return null instead, your code-chain becomes increasingly brief and still easy to read:

    double? val = Calc1() ?? Calc2() ?? Calc3() ?? Calc4();
    if(!val.HasValue) 
        throw new NoCalcsWorkedException();
    

    I used the following replacements for your functions, which results in the value 40.40 in val.

    static double? Calc1() { return null; /* failed */}
    static double? Calc2() { return null; /* failed */}
    static double? Calc3() { return null; /* failed */}
    static double? Calc4() { return 40.40; /* success! */}
    

    I realize that this solution won't always be applicable, but you posed a very interesting question and I believe, even though the thread is relatively old, that this is a pattern worth considering when you can make the amends.

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

    Create a list of delegates to your calculation functions and then have a while loop to cycle through them:

    List<Func<double>> calcMethods = new List<Func<double>>();
    
    // Note: I haven't done this in a while, so I'm not sure if
    // this is the correct syntax for Func delegates, but it should
    // give you an idea of how to do this.
    calcMethods.Add(new Func<double>(calc1));
    calcMethods.Add(new Func<double>(calc2));
    calcMethods.Add(new Func<double>(calc3));
    
    double val;
    for(CalcMethod calc in calcMethods)
    {
        try
        {
            val = calc();
            // If you didn't catch an exception, then break out of the loop
            break;
        }
        catch(GenericCalcException e)
        {
            // Not sure what your exception would be, but catch it and continue
        }
    
    }
    
    return val; // are you returning the value?
    

    That should give you a general idea of how to do it (i.e. it's not an exact solution).

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

    Given that the calculation methods have the same parameterless signature, you can register them in a list, and iterate through that list and execute the methods. Probably it would be even better for you to use Func<double> meaning "a function that returns a result of type double".

    using System;
    using System.Collections.Generic;
    
    namespace ConsoleApplication1
    {
      class CalculationException : Exception { }
      class Program
      {
        static double Calc1() { throw new CalculationException(); }
        static double Calc2() { throw new CalculationException(); }
        static double Calc3() { return 42.0; }
    
        static void Main(string[] args)
        {
          var methods = new List<Func<double>> {
            new Func<double>(Calc1),
            new Func<double>(Calc2),
            new Func<double>(Calc3)
        };
    
        double? result = null;
        foreach (var method in methods)
        {
          try {
            result = method();
            break;
          }
          catch (CalculationException ex) {
            // handle exception
          }
         }
         Console.WriteLine(result.Value);
       }
    }
    
    0 讨论(0)
  • 2020-12-12 13:20

    This looks like a job for... MONADS! Specifically, the Maybe monad. Start with the Maybe monad as described here. Then add some extension methods. I wrote these extension methods specifically for the problem as you described it. The nice thing about monads is you can write the exact extension methods needed for your situation.

    public static Maybe<T> TryGet<T>(this Maybe<T> m, Func<T> getFunction)
    {
        // If m has a value, just return m - we want to return the value
        // of the *first* successful TryGet.
        if (m.HasValue)
        {
            return m;
        }
    
        try
        {
            var value = getFunction();
    
            // We were able to successfully get a value. Wrap it in a Maybe
            // so that we can continue to chain.
            return value.ToMaybe();
        }
        catch
        {
            // We were unable to get a value. There's nothing else we can do.
            // Hopefully, another TryGet or ThrowIfNone will handle the None.
            return Maybe<T>.None;
        }
    }
    
    public static Maybe<T> ThrowIfNone<T>(
        this Maybe<T> m,
        Func<Exception> throwFunction)
    {
        if (!m.HasValue)
        {
            // If m does not have a value by now, give up and throw.
            throw throwFunction();
        }
    
        // Otherwise, pass it on - someone else should unwrap the Maybe and
        // use its value.
        return m;
    }
    

    Use it like so:

    [Test]
    public void ThrowIfNone_ThrowsTheSpecifiedException_GivenNoSuccessfulTryGet()
    {
        Assert.That(() =>
            Maybe<double>.None
                .TryGet(() => { throw new Exception(); })
                .TryGet(() => { throw new Exception(); })
                .TryGet(() => { throw new Exception(); })
                .ThrowIfNone(() => new NoCalcsWorkedException())
                .Value,
            Throws.TypeOf<NoCalcsWorkedException>());
    }
    
    [Test]
    public void Value_ReturnsTheValueOfTheFirstSuccessfulTryGet()
    {
        Assert.That(
            Maybe<double>.None
                .TryGet(() => { throw new Exception(); })
                .TryGet(() => 0)
                .TryGet(() => 1)
                .ThrowIfNone(() => new NoCalcsWorkedException())
                .Value,
            Is.EqualTo(0));
    }
    

    If you find yourself doing these sorts of calculations often, the maybe monad should reduce the amount of boilerplate code you have to write while increasing the readability of your code.

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