避免嵌套try catch块的模式?

时间:2011-10-17 16:03:36

标签: c# design-patterns try-catch monads

考虑我有三种(或更多)方法执行计算的情况,每种方法都会因异常而失败。为了尝试每次计算,直到我们找到一个成功,我一直在做以下事情:

double val;

try { val = calc1(); }
catch (Calc1Exception e1)
{ 
    try { val = calc2(); }
    catch (Calc2Exception e2)
    {
        try { val = calc3(); }
        catch (Calc3Exception e3)
        {
            throw new NoCalcsWorkedException();
        }
    }
}

是否有任何可接受的模式以更好的方式实现这一目标?当然,我可以将每个计算包装在一个辅助方法中,该方法在失败时返回null,然后只使用??运算符,但是有一种方法可以更一般地执行此操作(即无需为每个执行辅助方法)方法我想用)?我已经考虑过使用泛型编写一个静态方法,它在try / catch中包装任何给定的方法,并在失败时返回null,但我不确定如何解决这个问题。有什么想法吗?

16 个答案:

答案 0 :(得分:126)

尽量不要在控制流程或例外情况下使用例外。

但要直接回答你的问题(假设所有异常类型都相同):

Func<double>[] calcs = { calc1, calc2, calc3 };

foreach(var calc in calcs)
{
   try { return calc(); }
   catch (CalcException){  }
} 

throw new NoCalcsWorkedException();

答案 1 :(得分:37)

你可以通过将嵌套放入这样的方法来展平嵌套:

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();
}

但我怀疑真正的设计问题是存在三种不同的方法,这些方法基本上是相同的(从调用者的角度来看),但抛出不同的,无关的异常。

这假设三个例外 无关。如果他们都有一个共同的基类,那么最好使用一个带有单个catch块的循环,就像Ani建议的那样。

答案 2 :(得分:37)

只是提供一个“开箱即用”的替代方案,如何递归函数......

//Calling Code
double result = DoCalc();

double DoCalc(int c = 1)
{
   try{
      switch(c){
         case 1: return Calc1();
         case 2: return Calc2();
         case 3: return Calc3();
         default: return CalcDefault();  //default should not be one of the Calcs - infinite loop
      }
   }
   catch{
      return DoCalc(++c);
   }
}

注意:我绝不是说这是完成工作的最佳方式,只是不同的方式

答案 3 :(得分:20)

尽量不要根据异常来控制逻辑;另请注意,只有在特殊情况下才应抛出异常。在大多数情况下,计算不应抛出异常,除非它们访问外部资源或解析字符串或其他内容。无论如何,在最坏的情况下遵循TryMethod样式(如TryParse())来封装异常逻辑并使您的控制流程可维护和清洁:

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);

利用Try模式并组合方法列表而不是嵌套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;

如果foreach有点复杂,你可以说清楚:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }

答案 4 :(得分:9)

创建计算函数的委托列表,然后使用while循环遍历它们:

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?

这应该让你大致了解如何做到这一点(即它不是一个确切的解决方案)。

答案 5 :(得分:9)

这看起来像...... MONADS的工作!具体来说,也许monad。从Maybe monad as described here开始。然后添加一些扩展方法。我正如你所描述的那样专门为这个问题编写了这些扩展方法。关于monad的好处是你可以编写你的情况所需的确切扩展方法。

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;
}

像这样使用它:

[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));
}

如果您发现自己经常进行这些类型的计算,那么monad应该减少必须编写的样板代码量,同时提高代码的可读性。

答案 6 :(得分:7)

另一个版本的尝试方法。这个允许类型化异常,因为每个计算都有一个异常类型:

    public bool Try<T>(Func<double> func, out double d) where T : Exception
    {
      try
      {
        d = func();
        return true;
      }
      catch (T)
      {
        d = 0;
        return false;
      }
    }

    // usage:
    double d;
    if (!Try<Calc1Exception>(() = calc1(), out d) && 
        !Try<Calc2Exception>(() = calc2(), out d) && 
        !Try<Calc3Exception>(() = calc3(), out d))

      throw new NoCalcsWorkedException();
    }

答案 7 :(得分:4)

在Perl中,您可以执行foo() or bar(),如果bar()失败,它将执行foo()。在C#中,我们没有看到这个“if fail,then”构造,但是我们可以使用一个运算符来实现这个目的:null-coalesce operator ??,只有当第一部分为null时才会继续。< / p>

如果您可以更改计算的签名,并且如果您将其异常换行(如前面的帖子所示)或重写它们以返回null,那么您的代码链会变得越来越简短并且仍然易于阅读:

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

我为您的函数使用了以下替换,这导致40.40中的值为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! */}

我意识到这个解决方案并不总是适用,但你提出了一个非常有趣的问题,我相信,即使线程相对较旧,这是一个值得考虑的模式,当你可以进行修正时。

答案 8 :(得分:3)

鉴于计算方法具有相同的无参数签名,您可以在列表中注册它们,并遍历该列表并执行这些方法。可能使用Func<double>意味着“返回类型为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);
   }
}

答案 9 :(得分:3)

您可以使用Task / ContinueWith,并检查异常。这是一个很好的扩展方法,可以帮助它变得漂亮:

    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);
    }
}

答案 10 :(得分:1)

如果抛出的异常的实际类型无关紧要,您可以使用无类型的catch块:

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();

答案 11 :(得分:1)

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;
        }
    }
}

并像这样使用它:

using Utility;

...

TryHelper.TryUntilSuccessful(
    () =>
    {
        /* some code */
    },
    () =>
    {
        /* more code */
    },
    calc1,
    calc2,
    calc3,
    () =>
    {
        throw NotImplementedException();
    },
    ...
);

答案 12 :(得分:1)

OP的目的似乎是找到一个好的模式来解决他的问题并解决他当时正在努力解决的当前问题。

  

OP:&#34;我可以将每个计算包装在一个辅助方法中,该方法在失败时返回null,        然后只使用??运算符,但有没有更普遍的方法        (即不必为我想要使用的每种方法编写辅助方法)?        我已经考虑过使用包含任何给定的泛型的静态方法        try / catch中的方法,失败时返回null,        但我不确定我会怎么做。有什么想法吗?&#34;

我看到很多好的 模式避免了嵌套的try catch块 ,在这个Feed中发布,但没有找到问题的解决方案以上引用。  所以,这是解决方案:

如上所述,他想制作一个包装对象 ,在失败时返回null 。  我称之为 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;

但是,如果您想为CalcN()函数/方法返回的引用类型结果创建安全pod ,该怎么办。

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}";
}

因此,您可能会注意到 &#34;无需为您要使用的每种方法编写辅助方法&#34;

两种类型的广告(适用于ValueTypeResultReferenceTypeResult足够

以下是SafePod的代码。但它不是一个容器。相反,会为ValueTypeResultReferenceTypeResult创建一个异常安全的委托包装

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;
    }
}

这就是如何利用空合并运算符??以及一等公民实体(delegate s)的强大功能。

答案 13 :(得分:0)

你是正确的包装每个计算,但你应该根据tell-no-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();
    }
}

操作简单,可以独立改变行为。它们默认为什么并不重要。 作为证明,您可以将calc1DefaultingToCalc2实现为:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}

答案 14 :(得分:-1)

听起来你的计算有更多有效信息要返回,而不仅仅是计算本身。也许对他们来说更有意义的是做他们自己的异常处理并返回包含错误信息,值信息等的“结果”类。想想AsyncResult类跟随异步模式。然后,您可以评估计算的实际结果。您可以通过考虑如果计算失败,这就像它通过时那样具有信息性来合理化这一点。因此,例外是一条信息,而不是“错误”。

internal class SomeCalculationResult 
{ 
     internal double? Result { get; private set; } 
     internal Exception Exception { get; private set; }
}

...

SomeCalculationResult calcResult = Calc1();
if (!calcResult.Result.HasValue) calcResult = Calc2();
if (!calcResult.Result.HasValue) calcResult = Calc3();
if (!calcResult.Result.HasValue) throw new NoCalcsWorkedException();

// do work with calcResult.Result.Value

...

当然,我想知道更多关于你用来完成这些计算的整体架构。

答案 15 :(得分:-3)

如何跟踪您的行为......

double val;
string track = string.Empty;

try 
{ 
  track = "Calc1";
  val = calc1(); 

  track = "Calc2";
  val = calc2(); 

  track = "Calc3";
  val = calc3(); 
}
catch (Exception e3)
{
   throw new NoCalcsWorkedException( track );
}