web-dev-qa-db-fra.com

Modèle pour éviter les blocs de capture imbriqués?

Considérons une situation où j'ai trois (ou plus) façons d'effectuer un calcul, chacune pouvant échouer avec une exception. Afin d'essayer chaque calcul jusqu'à ce que nous trouvions celui qui réussit, j'ai fait ce qui suit:

double val;

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

Existe-t-il un modèle accepté qui y parvienne d'une manière plus agréable? Bien sûr, je pourrais encapsuler chaque calcul dans une méthode d'assistance qui renvoie null en cas d'échec, puis utiliser simplement le ??, mais existe-t-il un moyen de le faire de manière plus générale (c'est-à-dire sans avoir à écrire une méthode d'aide pour chaque méthode que je veux utiliser)? J'ai pensé à écrire une méthode statique à l'aide de génériques qui encapsule une méthode donnée dans un try/catch et retourne null en cas d'échec, mais je ne sais pas comment j'y arriverais. Des idées?

112
jjoelson

Dans la mesure du possible, n'utilisez pas d'exceptions pour le flux de contrôle ou des circonstances exceptionnelles.

Mais pour répondre directement à votre question (en supposant que tous les types d'exceptions sont les mêmes):

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

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

throw new NoCalcsWorkedException();
126
Ani

Vous pouvez aplatir l'emboîtement en le mettant dans une méthode comme celle-ci:

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

Mais je soupçonne que le problème de conception réel est l'existence de trois méthodes différentes qui font essentiellement la même chose (du point de vue de l'appelant) mais lèvent des exceptions différentes et sans rapport.

Cela suppose que les trois exceptions sont sans rapport. S'ils ont tous une classe de base commune, il serait préférable d'utiliser une boucle avec un seul bloc catch, comme l'a suggéré Ani.

37
Wyzard

Juste pour offrir une alternative "hors des sentiers battus", que diriez-vous d'une fonction récursive ...

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

REMARQUE: je ne dis en aucun cas que c'est la meilleure façon de faire le travail, juste une manière différente

37
musefan

Essayez de ne pas contrôler la logique en fonction des exceptions; notez également que les exceptions ne devraient être levées que dans des cas exceptionnels. Dans la plupart des cas, les calculs ne doivent pas lever d'exceptions, sauf s'ils accèdent à des ressources externes ou analysent des chaînes ou quelque chose. Quoi qu'il en soit, dans le pire des cas, suivez le style TryMethod (comme TryParse ()) pour encapsuler la logique d'exception et rendre votre flux de contrôle maintenable et propre:

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

Une autre variante utilisant le modèle Try et combinant la liste des méthodes au lieu de l'emboîtement si:

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;

et si le foreach est un peu compliqué, vous pouvez le dire clairement:

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

Cela ressemble à un travail pour ... MONADS! Plus précisément, la monade peut-être. Commencez avec la monade peut-être comme décrit ici . Ajoutez ensuite quelques méthodes d'extension. J'ai écrit ces méthodes d'extension spécifiquement pour le problème tel que vous l'avez décrit. La bonne chose à propos des monades est que vous pouvez écrire les méthodes d'extension exactes nécessaires à votre 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;
}

Utilisez-le comme ceci:

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

Si vous faites souvent ce genre de calculs, la monade peut-être devrait réduire la quantité de code standard que vous devez écrire tout en augmentant la lisibilité de votre code.

9
fre0n

Créez une liste de délégués à vos fonctions de calcul, puis disposez d'une boucle while pour les parcourir:

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?

Cela devrait vous donner une idée générale de la façon de le faire (c'est-à-dire que ce n'est pas une solution exacte).

9
Kiril

Une autre version de l'approche de la méthode try. Celui-ci permet des exceptions typées, car il existe un type d'exception pour chaque calcul:

    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

En Perl, vous pouvez faire foo() or bar(), qui exécutera bar() si foo() échoue. En C #, nous ne voyons pas cette construction "if fail, then", mais il y a un opérateur que nous pouvons utiliser à cet effet: l'opérateur null-coalesce ??, qui se poursuit uniquement si la première partie est nulle.

Si vous pouvez changer la signature de vos calculs et si vous encapsulez leurs exceptions (comme indiqué dans les articles précédents) ou les réécrivez pour retourner null à la place, votre chaîne de code devient de plus en plus brève et toujours facile à lire:

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

J'ai utilisé les remplacements suivants pour vos fonctions, ce qui donne la valeur 40.40 dans 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! */}

Je me rends compte que cette solution ne sera pas toujours applicable, mais vous avez posé une question très intéressante et je crois, même si le fil est relativement ancien, que c'est un modèle à considérer lorsque vous pouvez faire amende honorable.

4
Abel

Étant donné que les méthodes de calcul ont la même signature sans paramètre, vous pouvez les enregistrer dans une liste, parcourir cette liste et exécuter les méthodes. Il serait probablement encore mieux pour vous d'utiliser Func<double> signifiant "une fonction qui renvoie un résultat de 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);
   }
}
3
Marcin Seredynski

Vous pouvez utiliser un Task/ContinueWith et vérifier l'exception. Voici une belle méthode d'extension pour aider à la rendre jolie:

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

Et utilisez-le comme ceci:

using Utility;

...

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

Si le type réel de l'exception levée n'a pas d'importance, vous pouvez simplement utiliser un bloc catch sans type:

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

Il semble que l'intention du PO était de trouver un bon modèle pour résoudre son problème et résoudre le problème actuel avec lequel il se débattait à ce moment-là.

OP: "Je pourrais encapsuler chaque calcul dans une méthode d'assistance qui renvoie null en cas d'échec, puis utiliser simplement le ??, mais existe-t-il un moyen de le faire de manière plus générale (c'est-à-dire sans avoir à écrire une méthode d'aide pour chaque méthode que je veux utiliser)? J'ai pensé à écrire une méthode statique à l'aide de génériques qui encapsule une méthode donnée dans un try/catch et retourne null en cas d'échec, mais je ne sais pas comment j'y arriverais. Des idées?"

J'ai vu beaucoup de bons modèles qui évitent les blocs de capture d'essai imbriqués , publiés dans ce flux, mais je n'ai pas trouvé de solution au problème cité ci-dessus. Voici donc la solution:

Comme OP mentionné ci-dessus, il voulait créer un objet wrapper qui renvoie null en cas d'échec . Je l'appellerais pod (pod d'exception).

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;

Mais que se passe-t-il si vous souhaitez créer un pod sécurisé pour un résultat du type de référence renvoyé par les fonctions/méthodes 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}";
}

Ainsi, vous remarquerez peut-être qu'il n'est pas nécessaire "d'écrire une méthode d'assistance pour chaque méthode que vous souhaitez utiliser" .

Les deux types de pods (pour ValueTypeResults et ReferenceTypeResults) sont assez.


Voici le code de SafePod. Ce n'est pas un conteneur cependant. Au lieu de cela, il crée un wrapper délégué d'exception pour les deux ValueTypeResults et 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;
    }
}

Voilà comment vous pouvez exploiter l'opérateur de coalescence nulle ?? combiné avec la puissance de citoyen de première classe entités (delegates).

1
AndreyWD

Vous avez raison d'encapsuler chaque calcul, mais vous devez encapsuler selon le principe tell-don't-ask-principe.

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

Les opérations sont simples et peuvent modifier leur comportement indépendamment. Et peu importe pourquoi ils sont par défaut. Comme preuve, vous pouvez implémenter calc1DefaultingToCalc2 comme:

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