避免嵌套 try catch 块的模式?

发布于 2024-12-10 12:27:10 字数 589 浏览 2 评论 0原文

考虑这样一种情况,我有三种(或更多)方法来执行计算,每种方法都可能失败并出现异常。为了尝试每一项计算直到找到成功的计算,我一直在执行以下操作:

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,但我不确定如何解决这个问题。有什么想法吗?

Consider a situation where I have three (or more) ways of performing a calculation, each of which can fail with an exception. In order to attempt each calculation until we find one that succeeds, I have been doing the following:

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(16

帅哥哥的热头脑 2024-12-17 12:27:10

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

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

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

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

throw new NoCalcsWorkedException();

As far as possible, don't use exceptions for control flow or unexceptional circumstances.

But to answer your question directly (assuming all the exception-types are the same):

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

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

throw new NoCalcsWorkedException();
韵柒 2024-12-17 12:27:10

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

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

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

Just to offer an "outside the box" alternative, how about a recursive function...

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

NOTE: I am by no means saying that this is the best way to get the job done, just a different way

秋风の叶未落 2024-12-17 12:27:10

您可以通过将嵌套放入这样的方法中来展平嵌套:

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

You could flatten out the nesting by putting it into a method like this:

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

But I suspect the real design problem is the existence of three different methods that do essentially the same thing (from the caller's perspective) but throw different, unrelated exceptions.

This is assuming the three exceptions are unrelated. If they all have a common base class, it'd be better to use a loop with a single catch block, as Ani suggested.

无声情话 2024-12-17 12:27:10

尽量不要根据异常来控制逻辑;还要注意的是,只有在特殊情况下才应该抛出异常。大多数情况下,计算不应抛出异常,除非它们访问外部资源或解析字符串或其他内容。无论如何,在最坏的情况下,请遵循 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;
        }

Try not to control logic based on exceptions; note also that exceptions should be thrown only in exceptional cases. Calculations in most cases should not throw exceptions unless they access external resources or parse strings or something. Anyway in the worst case follow the TryMethod style (like TryParse()) to encapsulate exception logic and make your control flow maintainable and clean:

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

Another variation utilizing the Try pattern and combining list of methods instead of nested 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;

and if the foreach is a little complicated you can make it plain:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }
缪败 2024-12-17 12:27:10

创建计算函数的委托列表,然后使用 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?

这应该让您大致了解如何执行此操作(即,这不是一个精确的解决方案)。

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

浅唱ヾ落雨殇 2024-12-17 12:27:10

这看起来像是……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));
}

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

This looks like a job for... MONADS! Specifically, the Maybe monad. Start with the Maybe monad as described here. Then add some extension methods. I wrote these extension methods specifically for the problem as you described it. The nice thing about monads is you can write the exact extension methods needed for your situation.

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

Use it like so:

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

If you find yourself doing these sorts of calculations often, the maybe monad should reduce the amount of boilerplate code you have to write while increasing the readability of your code.

花期渐远 2024-12-17 12:27:10

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

Another version of the try method approach. This one allows typed exceptions, since there is an exception type for each calculation:

    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();
    }
浅沫记忆 2024-12-17 12:27:10

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

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

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

In Perl you can do foo() or bar(), which will execute bar() if foo() fails. In C# we don't see this "if fail, then" construct, but there's an operator that we can use for this purpose: the null-coalesce operator ??, which continues only if the first part is null.

If you can change the signature of your calculations and if you either wrap their exceptions (as shown in previous posts) or rewrite them to return null instead, your code-chain becomes increasingly brief and still easy to read:

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

I used the following replacements for your functions, which results in the value 40.40 in val.

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 realize that this solution won't always be applicable, but you posed a very interesting question and I believe, even though the thread is relatively old, that this is a pattern worth considering when you can make the amends.

夜深人未静 2024-12-17 12:27:10

鉴于计算方法具有相同的无参数签名,您可以将它们注册在列表中,然后迭代该列表并执行方法。也许使用 Func 会更好,意思是“返回 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);
   }
}

Given that the calculation methods have the same parameterless signature, you can register them in a list, and iterate through that list and execute the methods. Probably it would be even better for you to use Func<double> meaning "a function that returns a result of type 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);
   }
}
萝莉病 2024-12-17 12:27: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);
    }
}

You can use a Task/ContinueWith, and check for the exception. Here's a nice extension method to help make it pretty:

    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);
    }
}
坐在坟头思考人生 2024-12-17 12:27:10

如果抛出的异常的实际类型并不重要,您可以使用无类型的 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 the actual type of the exception thrown doesn't matter, you can just use a typeless catch block:

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();
网白 2024-12-17 12:27:10
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();
    },
    ...
);
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;
        }
    }
}

And use it like so:

using Utility;

...

TryHelper.TryUntilSuccessful(
    () =>
    {
        /* some code */
    },
    () =>
    {
        /* more code */
    },
    calc1,
    calc2,
    calc3,
    () =>
    {
        throw NotImplementedException();
    },
    ...
);
奶气 2024-12-17 12:27:10

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

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

我在此提要中看到了许多好的避免嵌套 try catch 块的模式,但没有找到所引用问题的解决方案多于。
所以,解决方案如下:

正如上面提到的,他想要创建一个包装对象在失败时返回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() 函数/方法返回的引用类型结果创建一个安全 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}";
}

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

两种类型的 pod(对于 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;
    }
}

这就是您如何利用空合并运算符 ??一等公民实体(代表)的力量相结合。

It seems that the OP's intention was to find a good pattern for solving his issue and resolving the current problem that he was struggling with at that moment.

OP: "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?"

I saw a lot of good patterns that avoid nested try catch blocks, posted in this feed, but didn't find a solution to the problem that is cited above.
So, here is the solution:

As OP mentioned above, he wanted to make a wrapper object which returns null on failure.
I would call it a pod (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;

But what if you'd like to create a safe pod for a Reference Type result returned by CalcN() functions/methods.

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

So, you might notice that there is no need "to write a helper method for each method you want to use".

The two types of pods (for ValueTypeResults and ReferenceTypeResults) are enough.


Here is the code of SafePod. It isn't a container though. Instead, it creates an exception-safe delegate wrapper for both ValueTypeResults and 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;
    }
}

That's how you can leverage the null-coalescing operator ?? combined with the power of first-class citizen entities (delegates).

绮烟 2024-12-17 12:27:10

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

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

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

The operations are simple, and can change their behaviour independently. And it doesn't matter why they default.
As a prove you could implement calc1DefaultingToCalc2 as:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}
捎一片雪花 2024-12-17 12:27:10

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

...

当然,我想更多地了解您用来完成这些计算的整体架构。

It sounds like your calculations have more valid information to return than just the calculation itself. Perhaps it would make more sense for them to do their own exception handling and return a "results" class that contains error information, value information, etc. Think like the AsyncResult class does following the async pattern. You can then evaluate the real result of the calculation. You can rationalize this by thinking in terms that if a calculation fails, that's just as informational as if it passes. Therefore, an exception is a piece of information, not an "error."

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

...

Of course, I'm wondering more about the overall architecture that you're using to get these calculations done.

九歌凝 2024-12-17 12:27:10

追踪你所做的动作怎么样?

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

What about tracking the actions your doing...

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 );
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文