避免嵌套try catch块的模式?

jjo*_*son 112 c# monads design-patterns try-catch

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

double val;

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

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

Ani*_*Ani 126

尽可能不要在控制流程或非常规情况下使用例外.

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

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

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

throw new NoCalcsWorkedException();
Run Code Online (Sandbox Code Playgroud)

  • 这假定`Calc1Exception`,`Calc2Exception`和`Calc3Exception`共享一个公共基类. (15认同)
  • +1只是因为它说"不要使用控制流的例外",尽管我会使用"永远不会"而不是"尽可能". (6认同)
  • 没关系,我没有看到你的'return`陈述. (5认同)
  • 最重要的是,他采用了共同的签名 - 这并不是那么遥远.很好的回答. (3认同)

mus*_*fan 38

只是为了提供"开箱即用"的替代方案,递归函数怎么样......

//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);
   }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我不得不用一种语言实现"On Error Resume Next",我生成的代码看起来很像这样. (6认同)
  • 请不要使用switch语句来创建for循环. (4认同)
  • 使用switch语句进行循环是不可维护的 (3认同)

Wyz*_*a-- 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();
}
Run Code Online (Sandbox Code Playgroud)

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

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


Moh*_*bed 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);
Run Code Online (Sandbox Code Playgroud)

使用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;
Run Code Online (Sandbox Code Playgroud)

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

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }
Run Code Online (Sandbox Code Playgroud)


Kir*_*ril 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?
Run Code Online (Sandbox Code Playgroud)

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


fre*_*e0n 9

这看起来像...... MONADS的工作!具体来说,也许monad.从这里描述的Maybe monad开始.然后添加一些扩展方法.我正如你所描述的那样专门为这个问题编写了这些扩展方法.关于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;
}
Run Code Online (Sandbox Code Playgroud)

像这样使用它:

[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));
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我喜欢这个解决方案.然而,对于之前没有接触monad的人来说,这是相当不透明的,这意味着这在c#中远非惯用.我不希望我的一个同事学习单子只是为了修改这段愚蠢的代码.不过,这对于将来的参考非常有用. (2认同)

Ste*_*fan 7

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

    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();
    }
Run Code Online (Sandbox Code Playgroud)