模式避免嵌套尝试捕获块?


114

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

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,但是我不确定如何处理。有任何想法吗?


您可以提供一些有关计算的细节吗?
詹姆士·约翰逊

2
基本上,它们只是解决/逼近PDE的不同方法。它们来自第3方库,因此我无法更改它们以返回错误代码或null。我能做的最好的办法是将每个包裹分别包装在一个方法中。
jjoelson 2011年

calc方法是否属于您的项目的一部分(而不是第三方库)?如果是这样,您可以提取引发异常的逻辑,然后使用该逻辑来确定需要调用哪个calc方法。
克里斯,

1
还有另外一个用例的这一点,我所遇到的在Java中-我需要解析StringDate使用SimpleDateFormat.parse,我需要尝试以几种不同的格式,在移动到下一个时抛出异常。
可悲变量

Answers:


125

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

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

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

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

throw new NoCalcsWorkedException();

15
这是假设Calc1ExceptionCalc2Exception以及Calc3Exception共享一个公共基类。
Wyzard

3
最重要的是,他假设有一个共同的签名-其实相差不远。好答案。
TomTom

1
此外,我continue在catch块中添加了catch块,并break在catch块之后添加了一个代码,以便在计算有效时结束循环(这要感谢Lirik)
jjoelson 2011年

6
+1只是因为它说“不要为控制流使用异常”,尽管我会使用“从不”而不是“尽可能”。
Bill K

1
@jjoelson :(而不是完全没有声明)break后面calc();的声明可能更惯用。trycontinue
亚当·罗宾逊

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

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


6
我不得不用一种语言实现“ On Error Resume Next”,并且我生成的代码看起来很像这样。
Jacob Krall

4
请不要使用switch语句创建for循环。
杰夫·费兰德

3
循环使用switch语句是无法维护的
Mohamed Abed

1
我知道我的答案不是最有效的代码,但是对于此类事情再次使用try / catch块也不是最好的方法。不幸的是,OP正在使用第三方库,并且必须尽其所能来确保成功。理想情况下,可以先验证输入,然后选择正确的计算功能以确保输入不会失败-当然,您可以为了安全起见将所有内容放入try / catch中;)
musefan 2011年

1
return DoCalc(c++)等效于return DoCalc(c)-后增加的值将不会传递得更深。为了使其工作(并引入更多隐晦之处),它可能更像return DoCalc((c++,c))
Artur Czajka,2011年

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


1
+1:这是最干净,最精打细算的解决方案。我在这里看到的其他解决方案只是想变得可爱,IMO。正如OP所说的,他没有编写API,所以他被抛出的异常所困扰。
Nate CK

19

尽量不要根据异常控制逻辑;还请注意,仅在特殊情况下才应引发异常。在大多数情况下,除非访问外部资源或分析字符串或其他内容,否则计算不应引发异常。无论如何,在最坏的情况下,请遵循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模式并结合方法列表而不是嵌套方法的变体:

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

老实说,我认为这只会导致不必要的抽象。这不是一个可怕的解决方案,但是在大多数情况下我不会使用它。
user606723 2011年

如果您不关心异常类型,而只想处理条件代码..则在抽象性和可维护性方面,将其成功转换为带条件的方法(无论是否成功),肯定会更好您可以使用清晰的描述性方法来隐藏处理混乱语法的异常..那么您的代码将对其进行处理,因为它是常规的条件方法。
Mohamed Abed

我知道要点,它们是有效的。但是,当在几乎所有地方都使用这种抽象类型(隐藏混乱/复杂性)时,它变得很荒谬,而要了解软件的功能变得更加困难。如我所说,这不是一个糟糕的解决方案,但我不会轻易使用它。
2011年

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?

那应该给你一个大概的想法(即这不是一个精确的解决方案)。


1
当然,除了您通常永远不应该抓住的事实Exception。;)
DeCaf 2011年

正如我所说的@DeCaf:“应该给您一个大概的方法(即不是一个确切的解决方案)”。因此,OP可以捕获发生任何适当的异常……而无需捕获泛型Exception
基里尔

是的,很抱歉,只是觉得有必要把它弄出来。
DeCaf 2011年

1
@DeCaf,对于那些可能不熟悉最佳实践的人来说,这是一个有效的说明。谢谢:)
Kiril

9

这看起来像是... MONADS的工作!特别是Maybe 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;
}

像这样使用它:

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


2
我喜欢这个解决方案。但是,对于以前从未接触过monad的人来说,它是相当不透明的,这意味着这与c#中的惯用语言相去甚远。我不希望我的任何一个同事学到monad只是为了将来修改这段愚蠢的代码。不过,这非常适合将来参考。
jjoelson 2011年

1
+1表示幽默感,用于为该问题编写尽可能最晦涩和冗长的解决方案,然后说“将减少您必须编写的样板代码,同时增加代码的可读性”。
Nate CK

1
嘿,我们不会抱怨System.Linq中潜藏着大量的代码,并整日快乐地使用这些monad。我认为@ fre0n只是意味着,如果您愿意将Maybe monad放入您的工具箱中,则这些链接在一起的评估将变得更易于查看和推理。有几种容易抓住的障碍。
塞巴斯蒂安·古德

仅仅因为它使用Maybe并不能使它成为单子解决方案。它使用的单调属性的零,Maybe因此也可以只使用null。此外,使用“monadicly”这将是Maybe。真正的单子解决方案将必须使用将第一个非异常值作为其状态的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();
    }

您实际上可以通过&&在每个条件之间使用来避免嵌套的ifs 。
DeCaf 2011年

4

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

如果您可以更改计算的签名,并且包装它们的异常(如前几篇文章所示)或重写它们以返回null,则您的代码链将变得越来越简短,并且仍然易于阅读:

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

我用下面的替代你的函数,这导致价值40.40val

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

我意识到这种解决方案并不总是适用,但是您提出了一个非常有趣的问题,我相信,即使线程相对较旧,这也是值得修正的模式。


1
我只想说“谢谢”。 我试图实现你在说什么。我希望我能正确理解。
AlexMelw

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

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

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

那不是总是调用列表中的每个函数吗?可能想在if(succeeded) { break; }捕捞后投掷(双关语不是故意的)。
的CVn

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

1

看来,OP的目的是寻找一个解决他的问题并解决他当时正在努力解决的当前问题的良好模式。

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

我看到了很多很好的模式,这些模式避免了嵌套在该提要中的try try catch块,但是没有找到解决上面提到的问题的方法。所以,这是解决方案:

如前所述,他想制作一个包装对象,该null对象在failure时返回。我将其称为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()函数/方法返回的引用类型结果创建一个安全的容器,该怎么办?

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

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

两种类型的豆荚(用于ValueTypeResultS和ReferenceTypeResultS)是足够的


这是的代码SafePod。它不是一个容器。而是为s和s 创建一个异常安全的委托包装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;
    }
}

这就是您可以利用空值促销运算符??一流公民实体(delegates)的功能相结合的方式。


0

包装每个计算结果是正确的,但应根据“不要问”的原则包装。

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

-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

...

当然,我想知道有关用于完成这些计算的总体体系结构的更多信息。


没关系-与OP建议的计算结果类似。我只喜欢这样的东西while (!calcResult.HasValue) nextCalcResult(),而不是Calc1,Calc2,Calc3等的列表。–
Kirk Broadhurst

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

4
这有什么帮助?如果calc1()失败,将永远不会执行cals2!
DeCaf 2011年

这不能解决问题。如果calc2失败,则仅执行calc1;如果calc1 && calc2失败,则仅执行calc3。
杰森

+1 orn。这就是我要做的。我只需要编写一个 catch,即将消息发送给我(track在这种情况下),并且我确切地知道代码中的哪个段导致了块失败。也许您应该详尽地告诉像DeCaf这样的成员,该track消息已发送到您的自定义错误处理例程,该例程使您可以调试代码。听起来他不了解您的逻辑。
jp2code 2011年

好吧,@ DeCaf是正确的,我的代码段无法继续执行jjoelson要求的下一个函数,因此我的解决方案不可行
Orn Kristjansson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.