避免嵌套try catch块的模式?

Posted

技术标签:

【中文标题】避免嵌套try catch块的模式?【英文标题】:Pattern to avoid nested try catch blocks? 【发布时间】:2011-12-09 10:22:30 【问题描述】:

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

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,但我不确定我将如何去做。有什么想法吗?

【问题讨论】:

你能提供一些关于计算的细节吗? 它们基本上只是求解/逼近 PDE 的不同方法。它们来自第 3 方库,因此我无法更改它们以返回错误代码或 null。我能做的最好的就是将每个单独包装在一个方法中。 计算方法是您项目的一部分(而不是第三方库)吗?如果是这样,您可以提取引发异常的逻辑并使用它来决定需要调用哪个 calc 方法。 我在 Java 中遇到了另一个用例——我需要使用 SimpleDateFormat.parseString 解析为 Date,并且我需要尝试几种不同的格式按顺序,当抛出异常时继续下一个。 【参考方案1】:

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

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

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

foreach(var calc in calcs)

   try  return calc(); 
   catch (CalcException)  
 

throw new NoCalcsWorkedException();

【讨论】:

这假定Calc1ExceptionCalc2ExceptionCalc3Exception 共享一个公共基类。 最重要的是,他假设了一个共同的签名 - 这并不是那么遥远。很好的答案。 另外,我在 catch 块中添加了continue,在 catch 块之后添加了break,以便在计算工作时循环结束(感谢 Lirik 的这一点) +1 只是因为它说“不要对控制流使用异常”,尽管我会使用“永远不会”而不是“尽可能”。 @jjoelson:calc(); 之后的break 语句在try 之后(并且根本没有continue 语句)可能更惯用。【参考方案2】:

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

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

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

这是假设三个例外不相关的。如果它们都有一个共同的基类,最好按照 Ani 的建议使用带有单个 catch 块的循环。

【讨论】:

+1:这是解决问题的最干净、最严肃的解决方案。我在这里看到的其他解决方案只是想变得可爱,IMO。正如 OP 所说,他没有编写 API,所以他被抛出的异常卡住了。 是的,有时我们无法解决“设计问题”。有时它甚至来自微软。以FindTimeZoneById 为例,它是 .NET Core/Framework 的一部分,如果我在 Windows 下传递 linux TimezoneID,然后在 linux 下传递 Windows timzoneId,或在 MacOS 下传递 Posix 时区等,它只会失败并出现异常。必须尝试一切:( 【参考方案3】:

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

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

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

【讨论】:

我不得不用一种语言实现一次“On Error Resume Next”,我生成的代码看起来很像这样。 请永远不要使用 switch 语句来创建 for 循环。 循环使用switch语句是不可维护的 我知道我的答案不是最有效的代码,但是对于这种事情再次使用 try/catch 块并不是最好的方法。不幸的是,OP 正在使用 3rd 方库,并且必须尽其所能确保成功。理想情况下,可以首先验证输入并选择正确的计算函数以确保它不会失败 - 当然,为了安全起见,您可以将所有这些都放在 try/catch 中;) return DoCalc(c++) 等价于 return DoCalc(c) - 后增量值不会被传递得更深。为了让它发挥作用(并引入更多的晦涩),它可能更像return DoCalc((c++,c))【参考方案4】:

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

【讨论】:

老实说,我认为这只会导致不必要的抽象。这不是一个糟糕的解决方案,但我不会在大多数情况下使用它。 如果您不关心异常类型,而只想处理条件代码..因此,将其转换为具有返回是否的条件方法在抽象和可维护性方面肯定会更好不管成功与否,这样你就可以用一个清晰​​的描述性方法隐藏异常处理混乱的语法......然后你的代码将处理它,因为它是一个常规的条件方法。 我知道这些要点,它们是有效的。然而,当几乎在任何地方使用这种类型的抽象(隐藏混乱/复杂性)时,它变得荒谬并且理解一个软件的本质变得更加困难。正如我所说,这不是一个糟糕的解决方案,但我不会轻易使用它。【参考方案5】:

为你的计算函数创建一个委托列表,然后有一个 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?

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

【讨论】:

当然除了你通常永远不应该抓住Exception这一事实。 ;) @DeCaf 正如我所说:“应该让你大致了解如何做到这一点(即不是一个精确的解决方案)。”所以 OP 可以捕捉到任何适当的异常......无需捕捉通用的Exception 是的,对不起,只是觉得有必要把它拿出来。 @DeCaf,对于那些可能不太熟悉最佳实践的人来说,这是一个有效的澄清。谢谢:)【参考方案6】:

这看起来像是……MONADS 的工作!具体来说,Maybe 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));

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

【讨论】:

我喜欢这个解决方案。但是,对于以前没有接触过 monad 的人来说,它是相当不透明的,这意味着这与 c# 中的惯用语相去甚远。我不希望我的一位同事学习 monad 只是为了在将来修改这一段愚蠢的代码。不过,这非常适合将来参考。 +1 幽默感,为这个问题写出最迟钝和冗长的解决方案,然后说它将“减少你必须编写的样板代码的数量,同时增加你的可读性代码”。 嘿,我们不会抱怨 System.Linq 中潜伏着大量代码,而是整天愉快地使用这些 monad。我认为@fre0n 只是意味着如果您愿意将 Maybe monad 放入您的工具包中,那么这些链式评估将变得更容易查看和推理。有几个很容易掌握的实现。 仅仅因为它使用Maybe 并不能使它成为一元解决方案;它使用了Maybe 的零个一元属性,所以也可以只使用null。此外,“单子”使用这将是Maybeinverse。真正的单子解决方案必须使用将第一个非异常值作为其状态的 State 单子,但是当正常的链式评估工作时,这将是矫枉过正。【参考方案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();
    

【讨论】:

您实际上可以通过在每个条件之间使用 &amp;&amp; 来避免嵌套 if。【参考方案8】:

在 Perl 中,您可以执行 foo() or bar(),如果 foo() 失败,它将执行 bar()。在 C# 中,我们看不到这种“如果失败,则”构造,但我们可以使用一个运算符来实现此目的:null-coalesce 运算符??,它仅在第一部分为空时才继续。

如果您可以更改计算的签名,并且如果您包装它们的异常(如之前的帖子所示)或重写它们以返回 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! */

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

【讨论】:

我只想说声“谢谢”。I tried to implement what you were talking about。我希望我理解正确。【参考方案9】:

鉴于计算方法具有相同的无参数签名,您可以将它们注册到一个列表中,然后遍历该列表并执行这些方法。使用Func&lt;double&gt; 表示“返回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);
   

【讨论】:

【参考方案10】:

您可以使用 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);
    

【讨论】:

【参考方案11】:

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

【讨论】:

这不是总是调用列表中的每个函数吗?可能想在if(succeeded) break; post-catch 之类的东西中抛出(双关语不是有意的)。【参考方案12】:
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();
    ,
    ...
);

【讨论】:

【参考方案13】:

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

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

我看到了很多很好的避免嵌套 try catch 块的模式,张贴在此提要中,但没有找到解决上述问题的方法. 所以,这里是解决方案:

正如上面提到的 OP,他想制作一个包装器对象在失败时返回 null。 我将其称为 pod异常安全的 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() 函数/方法返回的 Reference Type 结果 创建一个安全 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";

因此,您可能会注意到没有必要“为您要使用的每个方法编写辅助方法”

两种类型的 podValueTypeResults 和 ReferenceTypeResults)足够


这里是SafePod的代码。虽然它不是一个容器。相反,它ValueTypeResults 和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;
    


这就是您可以利用空合并运算符 ??一等公民 实体 (delegates) 的力量相结合的方式。

【讨论】:

【参考方案14】:

你对每个计算进行包装是对的,但你应该根据告诉-不要-询问-原则进行包装。

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

【讨论】:

【参考方案15】:

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

...

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

【讨论】:

这没关系 - 就包装计算而言,类似于 OP 的建议。我更喜欢while (!calcResult.HasValue) nextCalcResult() 之类的东西,而不是 Calc1、Calc2、Calc3 等的列表。【参考方案16】:

那么跟踪你的行为呢……

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

【讨论】:

这有什么帮助?如果 calc1() 失败 cals2 将永远不会被执行! 这并不能解决问题。仅在 calc2 失败时执行 calc1,仅在 calc1 && calc2 失败时执行 calc3。 +1 鸟。这就是我所做的。我只需要编写 一个 代码来捕获发送给我的消息(在这种情况下为track),并且我确切地知道我的代码中的哪个段导致块失败.也许您应该详细说明告诉像 DeCaf 这样的成员 track 消息被发送到您的自定义错误处理例程,使您能够调试代码。听起来他不明白你的逻辑。 好吧,@DeCaf 是正确的,我的代码段没有继续执行 jjoelson 要求的下一个函数,因为我的解决方案不可行

以上是关于避免嵌套try catch块的模式?的主要内容,如果未能解决你的问题,请参考以下文章

在 XDocument 中搜索 XML 节点时避免 Try n Catch

除了try{}catch{},你究竟还知道多少避免空指针异常的骚操作?

除了try{}catch{},你究竟还知道多少避免空指针异常的骚操作?

除了try{}catch{},你究竟还知道多少避免空指针异常的骚操作?

避免嵌套的 try...finally 块在 Delphi

JavaScript异常处理