避免嵌套try catch块的模式?
考虑一种情况,我有三种(或更多)的计算方法,每种方法都可能会失败并产生一个exception。 为了尝试每一个计算,直到find一个成功的,我一直在做以下工作:
double val; try { val = calc1(); } catch (Calc1Exception e1) { try { val = calc2(); } catch (Calc2Exception e2) { try { val = calc3(); } catch (Calc3Exception e3) { throw new NoCalcsWorkedException(); } } }
有没有一种更好的方式来达到这个目的? 当然,我可以将每个计算包装在一个帮助器方法中,在失败时返回null,然后使用??
运算符,但有没有办法更一般地这样做(即不必为每个我想使用的方法编写一个帮助器方法)? 我想写一个静态方法使用generics封装在try / catch中的任何给定的方法,并在失败时返回null,但我不知道如何去做这件事。 有任何想法吗?
尽可能不要在控制stream或exception情况下使用exception。
但要直接回答你的问题(假设所有的exceptiontypes都是一样的):
Func<double>[] calcs = { calc1, calc2, calc3 }; foreach(var calc in calcs) { try { return calc(); } catch (CalcException){ } } throw new NoCalcsWorkedException();
你可以通过把它放入这样一个方法来拼合出嵌套:
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(); }
但是我怀疑真正的devise问题是三种不同的方法的存在(从调用者的angular度来看本质上是相同的),但抛出了不同的,不相关的例外。
这是假设三个例外是无关的。 如果他们都有一个共同的基类,那么最好使用一个循环和一个catch块,就像Ani所build议的那样。
只是为了提供一个“盒子外”的select,如何recursion函数…
//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); } }
注:我绝不是说这是完成工作的最好方式,只是一种不同的方式
尽量不要根据exception来控制逻辑; 还要注意,只有在特殊情况下才应该抛出exception。 在大多数情况下,计算不应抛出exception,除非它们访问外部资源或parsingstring或其他东西。 无论如何,在最坏的情况下,按照TryMethod风格(如TryParse())来封装exception逻辑,并使您的控制stream可维护和清洁:
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模式和组合方法列表,而不是嵌套如果:
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; }
创build一个代表到你的计算函数的列表,然后有一个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?
这应该给你一个如何去做的总体思路(即它不是一个确切的解决scheme)。
这看起来像一个工作……蒙娜德斯! 具体来说,Maybe monad。 从这里描述的Maybe 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应该减less你写的样板代码的数量,同时增加代码的可读性。
try方法的另一个版本。 这一个允许types的exception,因为每个计算都有一个exceptiontypes:
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(); }
鉴于计算方法具有相同的无参数签名,您可以将它们注册到一个列表中,并遍历该列表并执行这些方法。 也许你会更好地使用Func<double>
表示“返回double
types结果的函数”。
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); } }
在Perl中,你可以做foo() or bar()
,如果foo()
失败,它将执行bar()
。 在C#中,我们没有看到这个“如果失败,然后”构造,但有一个操作符,我们可以用于这个目的:空coalesce操作符??
,只有在第一部分为空时才继续。
如果您可以更改计算的签名,并且如果您要封装它们的exception(如之前的文章中所示)或者重写它们以返回null
,那么您的代码链变得越来越简单,并且仍然易于阅读:
double? val = Calc1() ?? Calc2() ?? Calc3() ?? Calc4(); if(!val.HasValue) throw new NoCalcsWorkedException();
我为你的函数使用了下面的replace项,这会导致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! */}
我意识到这个解决scheme并不总是适用的,但是你提出了一个非常有趣的问题,我相信,即使线程比较老,这是一个值得考虑的模式,当你可以进行修改。
您可以使用任务/ ContinueWith,并检查exception。 这里有一个很好的扩展方法来帮助它:
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); } }
如果引发exception的实际types不重要,则可以使用无types的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();
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(); }, ... );
你对每一次计算都是正确的,但是你应该按照“不问不答”的原则进行包装。
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(); } }
操作简单,可以独立改变行为。 不要紧,为什么他们默认。 作为一个certificate你可以实现calc1DefaultingToCalc2为:
double calc1DefaultingToCalc2(){ try { val = calc2(); if(specialValue(val)){ val = calc2DefaultingToCalc3WithConvertedException() } } catch (Calc1Exception e1) { //defaulting to simpler method return calc2defaultingToCalc3WithConvertedException(); } }
这听起来像你的计算有更多的有效信息,而不仅仅是计算本身。 也许更有意义的是,他们做自己的exception处理,并返回一个包含错误信息,值信息等的“结果”类。像AsyncResult类一样遵循asynchronous模式。 然后您可以评估计算的实际结果。 你可以通过思考如果计算失败就可以合理化,就像通过信息一样。 因此,例外是一条信息,而不是“错误”。
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 ...
当然,我想知道更多关于你用来完成这些计算的整体架构。
跟踪你的行为
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 ); }