Pattern to avoid nested try catch blocks?

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

double val;


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

Is there any accepted pattern which achieves this in a nicer way? Of course 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?

20503 次浏览

尽量不要基于异常控制逻辑; 还要注意,只有在异常情况下才应该引发异常。在大多数情况下,计算不应引发异常,除非它们访问外部资源或解析字符串或其他东西。无论如何,在最糟糕的情况下,遵循 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);

另一个变体使用的尝试模式和组合列表的方法,而不是嵌套,如果:

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 (var tryCalc in tryCalcs)
{
if (tryCalc(out calcOutput)) break;
}

追踪你的行为怎么样。

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

尽可能不要对控制流或非异常情况使用异常。

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

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


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


throw new NoCalcsWorkedException();

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

您可以通过将嵌套放入如下方法来展平它:

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 建议的那样。

如果抛出的异常的实际类型无关紧要,那么可以使用无类型 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();

考虑到计算方法具有相同的无参数签名,您可以将它们注册到一个列表中,然后循环遍历该列表并执行这些方法。也许使用 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);
}
}

只是提供一个“开箱即用”的替代方案,递归函数怎么样..。

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

注意: 我绝不是说这是完成工作的最好方法,只是一种 与众不同方法

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

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

听起来你的计算要返回的有效信息比计算本身要多得多。也许对他们来说,做自己的异常处理并返回一个包含错误信息、值信息等的“ result”类更有意义。像 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


...

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

这看起来像是... MONADS 的工作!具体来说,就是“可能”单子。从可能单体 正如这里所描述的开始。然后添加一些扩展方法。正如您所描述的,我专门针对这个问题编写了这些扩展方法。单子的好处是你可以为你的情况编写精确的扩展方法。

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 应该减少您必须编写的样板代码的数量,同时提高代码的可读性。

在 Perl 中,您可以执行 foo() or bar(),如果 foo()失败,它将执行 bar()。在 C # 中,我们没有看到这种“如果失败,那么”的结构,但是我们可以使用一个操作符来实现这个目的: null-conesce 操作符 ??,它只在第一部分为 null 时才继续。

如果你可以改变你的计算的签名,如果你或者包装它们的异常(如前面的文章所示)或者改写它们返回 null,你的代码链变得越来越简短,仍然容易阅读:

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

我对函数使用了以下替换,结果是 val中的值 40.40

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! */}

我意识到这个解决方案并不总是适用的,但是你提出了一个非常有趣的问题,我相信,即使这个主题相对来说比较古老,但是这是一个值得考虑的模式,当你可以做出修正的时候。

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

操作很简单,可以独立地改变它们的行为,而且它们为什么违约并不重要。 作为证明,您可以将 calc1DefaultingToCalc2实现为:

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

您可以使用 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);
}
}
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();
},
...
);

似乎 OP 的意图是找到一个好的模式来解决他的问题和解决目前的问题,他正在与此刻。

OP: “我可以将每个计算包装在一个辅助方法中,该方法在失败时返回 null, 然后使用 ??操作符,但是有没有更通用的方法 (例如,不需要为我想要使用的每个方法编写一个助手方法) ? 我曾考虑过使用泛型编写一个静态方法来包装任何给定的内容 方法,并在失败时返回 null, but I'm not sure how I would go about this. Any ideas?"

我看到了很多好的 避免嵌套的 try catch 块的模式,张贴在这个饲料,但没有找到一个解决问题,是上面引用的解决方案。 所以,解决办法是这样的:

正如上面提到的 OP,他希望创建一个包装器对象 当失败时返回 null。 我会称之为 豆荚(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;

但是,如果您想为 CalcN ()函数/方法返回的 参考类型结果创建一个 安全舱,该怎么办呢。

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

因此,您可能会注意到不需要 < em > “为您想要使用的每个方法编写一个助手方法”

两种豆荚(对于 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;
}
}

That's how you can leverage the null-coalescing operator ?? combined with the power of 第一类物件 entities (delegates).