wake-up-neo.com

Muster, um verschachtelte try catch-Blöcke zu vermeiden?

Stellen Sie sich eine Situation vor, in der drei (oder mehr) Berechnungsmethoden zur Verfügung stehen, von denen jede mit einer Ausnahme fehlschlagen kann. Um jede Berechnung zu versuchen, bis wir eine finden, die erfolgreich ist, habe ich Folgendes getan:

double val;

try { val = calc1(); }
catch (Calc1Exception e1)
{ 
    try { val = calc2(); }
    catch (Calc2Exception e2)
    {
        try { val = calc3(); }
        catch (Calc3Exception e3)
        {
            throw new NoCalcsWorkedException();
        }
    }
}

Gibt es ein akzeptiertes Muster, das dies auf eine schönere Weise erreicht? Natürlich könnte ich jede Berechnung in eine Hilfsmethode einbinden, die bei einem Fehler null zurückgibt, und dann einfach das ?? Operator, aber gibt es eine Möglichkeit, dies allgemeiner zu tun (d. h. ohne für jede Methode, die ich verwenden möchte, eine Hilfsmethode schreiben zu müssen)? Ich habe darüber nachgedacht, eine statische Methode mit Generika zu schreiben, die eine bestimmte Methode in einen try/catch-Befehl einschließt und bei einem Fehler null zurückgibt. Ich bin mir jedoch nicht sicher, wie ich dabei vorgehen soll. Irgendwelche Ideen?

112
jjoelson

Verwenden Sie nach Möglichkeit keine Ausnahmen für Kontrollflüsse oder außergewöhnliche Umstände.

Aber um Ihre Frage direkt zu beantworten (unter der Annahme, dass alle Ausnahmetypen gleich sind):

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

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

throw new NoCalcsWorkedException();
126
Ani

Sie können die Verschachtelung reduzieren, indem Sie sie in eine der folgenden Methoden einfügen:

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

Aber ich vermute, das echte Designproblem ist die Existenz von drei verschiedenen Methoden, die im Wesentlichen dasselbe tun (aus der Sicht des Aufrufers), aber unterschiedliche, nicht verwandte Ausnahmen auslösen.

Dies setzt voraus, dass die drei Ausnahmen sind nicht zusammenhängen. Wenn alle eine gemeinsame Basisklasse haben, ist es besser, eine Schleife mit einem einzelnen catch-Block zu verwenden, wie Ani vorgeschlagen hat.

37
Wyzard

Wie wäre es mit einer rekursiven Funktion, um eine "außerhalb der Box" liegende Alternative anzubieten?.

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

HINWEIS: Ich sage keineswegs, dass dies der beste Weg ist, um die Arbeit zu erledigen, nur ein anderer Weg

37
musefan

Versuchen Sie, die Logik nicht anhand von Ausnahmen zu steuern. Beachten Sie auch, dass Ausnahmen nur in Ausnahmefällen ausgelöst werden sollten. Berechnungen sollten in den meisten Fällen keine Ausnahmen auslösen, es sei denn, sie greifen auf externe Ressourcen oder analysieren Zeichenfolgen oder ähnliches. Folgen Sie im schlimmsten Fall dem TryMethod-Stil (wie TryParse ()), um die Ausnahmelogik zu kapseln und Ihren Kontrollfluss wartbar und sauber zu machen:

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

Eine andere Variante, die das Try-Muster verwendet und die Liste der Methoden kombiniert, anstatt sie zu verschachteln, wenn:

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;

und wenn der foreach etwas kompliziert ist, kannst du es klarstellen:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }
20
Mohamed Abed

Das sieht nach einem Job für ... MONADS aus! Speziell die Vielleicht Monade. Beginnen Sie mit der Vielleicht-Monade wie hier beschrieben . Fügen Sie dann einige Erweiterungsmethoden hinzu. Ich habe diese Erweiterungsmethoden speziell für das von Ihnen beschriebene Problem geschrieben. Das Schöne an Monaden ist, dass Sie genau die Erweiterungsmethoden schreiben können, die für Ihre Situation erforderlich sind.

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

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

Wenn Sie feststellen, dass Sie diese Art von Berechnungen häufig durchführen, sollte die Monade möglicherweise die Menge des zu schreibenden Code reduzieren und gleichzeitig die Lesbarkeit Ihres Codes verbessern.

9
fre0n

Erstellen Sie eine Liste der Delegierten für Ihre Berechnungsfunktionen und führen Sie anschließend eine while-Schleife durch:

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?

Das sollte Ihnen eine allgemeine Vorstellung davon geben, wie es geht (d. H. Es ist keine exakte Lösung).

9
Kiril

Eine andere Version des try Methodenansatzes. Diese erlaubt typisierte Ausnahmen, da es für jede Berechnung einen Ausnahmetyp gibt:

    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

In Perl können Sie foo() or bar() ausführen, wodurch bar() ausgeführt wird, wenn foo() fehlschlägt. In C # sehen wir dieses Konstrukt "if fail, then" nicht, aber es gibt einen Operator, den wir für diesen Zweck verwenden können: den Null-Koaleszenz-Operator ??, Der nur fortgesetzt wird, wenn der erste Teil null ist.

Wenn Sie die Signatur Ihrer Berechnungen ändern und ihre Ausnahmen (wie in den vorherigen Beiträgen gezeigt) umbrechen oder sie neu schreiben, um stattdessen null zurückzugeben, wird Ihre Code-Kette immer kürzer und dennoch leicht lesbar:

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

Ich habe die folgenden Ersetzungen für Ihre Funktionen verwendet, die den Wert 40.40 In val ergeben.

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

Mir ist klar, dass diese Lösung nicht immer anwendbar ist, aber Sie haben eine sehr interessante Frage gestellt, und ich glaube, obwohl der Thread relativ alt ist, ist dies ein Muster, über das man nachdenken sollte, wenn man Abhilfe schaffen kann.

4
Abel

Da die Berechnungsmethoden dieselbe parameterlose Signatur haben, können Sie sie in einer Liste registrieren, diese Liste durchlaufen und die Methoden ausführen. Vielleicht ist es sogar noch besser, wenn Sie Func<double> Verwenden, was "eine Funktion, die ein Ergebnis vom Typ double zurückgibt" bedeutet.

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

Sie können einen Task/ContinueWith verwenden und nach der Ausnahme suchen. Hier ist eine nette Erweiterungsmethode, um es hübsch zu machen:

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

Und benutze es so:

using Utility;

...

TryHelper.TryUntilSuccessful(
    () =>
    {
        /* some code */
    },
    () =>
    {
        /* more code */
    },
    calc1,
    calc2,
    calc3,
    () =>
    {
        throw NotImplementedException();
    },
    ...
);
1
Ryan Lester

Wenn der tatsächliche Typ der ausgelösten Ausnahme keine Rolle spielt, können Sie einfach einen typenlosen catch-Block verwenden:

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

Es scheint, dass die Absicht des OP war, ein gutes Muster für die Lösung seines Problems und die Lösung des aktuellen Problems zu finden, mit dem er in diesem Moment zu kämpfen hatte.

OP: "Ich könnte jede Berechnung in eine Hilfsmethode einbinden, die bei einem Fehler null zurückgibt, und dann einfach den Operator ?? Verwenden, aber es gibt eine Möglichkeit, dies allgemeiner zu tun (dh ohne eine Hilfsmethode schreiben zu müssen Ich habe darüber nachgedacht, eine statische Methode mit Generika zu schreiben, die eine bestimmte Methode in einen try/catch-Befehl einschließt und bei einem Fehler null zurückgibt, aber ich bin mir nicht sicher, wie ich dabei vorgehen soll Ideen? "

Ich habe eine Menge guter Muster gesehen, die geschachtelte try catch-Blöcke vermeiden , die in diesem Feed gepostet wurden, aber nicht Finden Sie eine Lösung für das oben genannte Problem. Also, hier ist die Lösung:

Wie oben in OP erwähnt, wollte er ein Wrapper-Objekt erstellen, das bei einem Fehler null zurückgibt . Ich würde es Pod ( Ausnahmesicherer Pod) nennen.

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;

Was aber, wenn Sie einen sicheren Pod erstellen möchten für ein Referenztyp-Ergebnis, das von CalcN () -Funktionen/-Methoden zurückgegeben wird?.

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

Möglicherweise müssen Sie also nicht "für jede Methode, die Sie verwenden möchten, eine Hilfsmethode schreiben" =.

Die zwei Arten von Hülsen (für ValueTypeResults und ReferenceTypeResults) sind genug.


Hier ist der Code von SafePod. Es ist jedoch kein Container. Stattdessen wird mit ein ausnahmesicherer Delegaten-Wrapper erstellt sowohl für ValueTypeResults als auch 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;
    }
}

Auf diese Weise können Sie den Null-Koaleszenz-Operator ?? In Kombination mit der Macht von erstklassigen Bürgern Entities (delegates) nutzen.

1
AndreyWD

Sie haben Recht, wenn Sie jede Berechnung abschließen, aber Sie sollten nach dem Tell-Don't-Ask-Prinzip abschließen.

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

Die Operationen sind einfach und können ihr Verhalten unabhängig voneinander ändern. Und es spielt keine Rolle, warum sie in Verzug geraten. Als Beweis könnten Sie calc1DefaultingToCalc2 implementieren als:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}
0
raisercostin