web-dev-qa-db-ja.com

ネストされたtry catchブロックを回避するパターン?

計算を実行する3つ(またはそれ以上)の方法があり、それぞれが例外で失敗する可能性がある状況を考えてみましょう。成功するものが見つかるまで各計算を試みるために、私は次のことを行ってきました。

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を返す静的メソッドを書くことを考えましたが、どうすればよいかわかりません。何か案は?

112
jjoelson

可能な限り、制御フローや例外的状況には例外を使用しないでください。

しかし、あなたの質問に直接答えるために(すべての例外タイプが同じであると仮定して):

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

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

throw new NoCalcsWorkedException();
126
Ani

次のようなメソッドに入れ子にすることで、ネストをフラット化できます。

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

しかし、realの設計上の問題は、本質的に同じこと(呼び出し側の観点から)を行うが、異なる無関係な例外をスローする3つの異なるメソッドの存在だと思います。

これは、3つの例外are無関係を想定しています。それらすべてに共通の基本クラスがある場合は、Aniが提案したように、単一のcatchブロックでループを使用する方が良いでしょう。

37
Wyzard

「ボックス外」の代替手段を提供するために、再帰関数はどうでしょうか...

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

注:私はこれが仕事を成し遂げる最良の方法であると言っているわけではありません。ただdifferentの方法です

37
musefan

例外に基づいてロジックを制御しないようにしてください。また、例外は例外的な場合にのみスローされることに注意してください。ほとんどの場合、計算は、外部リソースにアクセスしたり、文字列などを解析しない限り、例外をスローしません。とにかく最悪の場合は、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;
        }
20
Mohamed Abed

これは... MONADSの仕事のようです!具体的には、多分モナド。 Maybeモナドで開始 ここで説明 。次に、いくつかの拡張メソッドを追加します。これらの拡張メソッドは、あなたが説明したように、この問題専用に作成しました。モナドの良いところは、あなたの状況に必要な正確な拡張メソッドを書くことができることです。

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

これらの種類の計算を頻繁に行うことに気付いた場合、多分モナドは、コードの可読性を高めながら、記述する必要のある定型コードの量を減らす必要があります。

9
fre0n

計算関数へのデリゲートのリストを作成し、それらを循環する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?

それはあなたにそれを行う方法の一般的なアイデアを与えるはずです(つまり、それは正確な解決策ではありません)。

9
Kiril

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

Perlでは、foo() or bar()を実行できます。これは、bar()が失敗した場合にfoo()を実行します。 C#では、この「失敗した場合」の構造は表示されませんが、この目的に使用できる演算子があります。null-coalesce演算子??。最初の部分がヌルの場合のみ継続します。

計算の署名を変更でき、例外をラップする(以前の投稿に示されているように)か、代わりにnullを返すように書き換えると、コードチェーンはますます短くなり、読みやすくなります。

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

関数に次の置換を使用した結果、値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! */}

このソリューションは常に適用できるとは限らないことを理解していますが、非常に興味深い質問を投げかけました。スレッドが比較的古いにもかかわらず、これはいつ修正できるかを検討する価値があるパターンだと思います。

4
Abel

計算メソッドが同じパラメータなしのシグネチャを持っている場合、それらをリストに登録し、そのリストを反復処理してメソッドを実行できます。 「double型の結果を返す関数」を意味する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);
   }
}
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);
    }
}
3
Dax Fohl
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
Ryan Lester

スローされた例外の実際のタイプが問題にならない場合は、タイプのない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();
1
Jacob Krall

OPの意図は、彼の問題を解決し、その時点で苦労していた現在の問題を解決するための良いパターンを見つけることであったようです。

OP:「失敗するとnullを返すヘルパーメソッドで各計算をラップし、??演算子を使用できますが、これをより一般的に行う方法があります(つまり、ヘルパーメソッドを記述する必要はありません。使用したい各メソッド)?ジェネリックを使用して、特定のメソッドをtry/catchでラップし、失敗するとnullを返す静的メソッドを書くことを考えましたが、どうすればよいのかわかりません。 ?」

このフィードに投稿された良いネストされたtry catchブロックを回避するパターンを見ましたが、上記の問題の解決策が見つかりませんでした。だから、ここに解決策があります:

前述のOPのように、彼はラッパーオブジェクトを作成したかった失敗するとnullを返します。私はそれをポッド例外セーフポッド)と呼びます。

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結果に対して安全なポッドを作成したい場合はどうでしょう。

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

したがって、"使用するメソッドごとにヘルパーメソッドを記述する"が必要ないことに気付くかもしれません。

2種類のポッドValueTypeResultsおよび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;
    }
}

これが、null結合演算子??ファーストクラスの市民エンティティ(delegates)の力と組み合わせて活用する方法です。

1
AndreyWD

各計算をラップするのは正しいですが、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();
    }
}
0
raisercostin