Patrón para evitar anidados try catch bloques?


Considere una situación en la que tengo tres (o más) formas de realizar un cálculo, cada una de las cuales puede fallar con una excepción. Para intentar cada cálculo hasta que encontremos uno que tenga éxito, he estado haciendo lo siguiente:

double val;

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

¿Hay algún patrón aceptado que logre esto de una manera más agradable? Por supuesto, podría envolver cada cálculo en un método auxiliar que devuelve null en caso de fallo, y luego usar el operador ??, pero ¿hay una forma de hacer esto de manera más general (es decir, sin tener que escribir un método de ayuda para cada método que quiero usar)? He pensado en escribir un método estático usando genéricos que envuelve cualquier método dado en un try / catch y devuelve null en caso de fallo, pero no estoy seguro de cómo lo haría. Alguna idea?

Author: aculich, 2011-10-17

16 answers

En la medida de lo posible, no utilice excepciones para controlar el flujo o circunstancias no excepcionales.

Pero para responder tu pregunta directamente (asumiendo que todos los tipos de excepción son los mismos):

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

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

throw new NoCalcsWorkedException();
 125
Author: Ani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 16:11:06

Podría aplanar el anidamiento poniéndolo en un método como este:

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

Pero sospecho que el problema de diseño real es la existencia de tres métodos diferentes que hacen esencialmente lo mismo (desde la perspectiva de la persona que llama) pero arrojan excepciones diferentes y no relacionadas.

Esto es asumiendo que las tres excepciones son no relacionadas. Si todos tienen una clase base común, sería mejor usar un bucle con un solo bloque catch, como sugirió Ani.

 37
Author: Wyzard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 16:17:00

Solo para ofrecer una alternativa "fuera de la caja", qué tal una función recursiva...

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

NOTA: De ninguna manera estoy diciendo que esta es la mejor manera de hacer el trabajo, solo una diferente manera

 37
Author: musefan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-23 10:42:37

Trate de no controlar la lógica basada en excepciones; tenga en cuenta también que las excepciones deben lanzarse solo en casos excepcionales. Los cálculos en la mayoría de los casos no deben lanzar excepciones a menos que accedan a recursos externos o analicen cadenas o algo así. De todos modos, en el peor de los casos, siga el estilo TryMethod (como TryParse ()) para encapsular la lógica de excepción y hacer que su flujo de control sea mantenible y limpio:

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

Otra variación utilizando el patrón Try y combinando la lista de métodos en su lugar de si anidado:

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;

Y si el foreach es un poco complicado puede hacerlo claro:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }
 20
Author: Mohamed Abed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-18 10:05:54

Cree una lista de delegados a sus funciones de cálculo y luego tenga un bucle while para recorrerlas:

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?

Eso debería darte una idea general de cómo hacerlo (es decir, no es una solución exacta).

 9
Author: Kiril,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 23:08:54

Esto parece un trabajo para... ¡MÓNADAS! Específicamente, la mónada Tal vez. Comience con la mónada Maybe como se describe aquí. Luego agregue algunos métodos de extensión. Escribí estos métodos de extensión específicamente para el problema como lo describió. Lo bueno de las mónadas es que puede escribir los métodos de extensión exactos necesarios para su situación.

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

Úsalo así:

[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 te encuentras haciendo este tipo de cálculos a menudo, la mónada tal vez debería reducir la cantidad de código repetitivo que tiene que escribir mientras aumenta la legibilidad de su código.

 9
Author: fre0n,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-18 02:42:55

Otra versión del método try. Este permite excepciones escritas, ya que hay un tipo de excepción para cada cálculo:

    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
Author: Stefan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 16:20:27

En Perl puedes hacer foo() or bar(), que ejecutará bar() si foo() falla. En C# no vemos esta construcción "if fail, then", pero hay un operador que podemos usar para este propósito: el operador null-coalesce ??, que continúa solo si la primera parte es null.

Si puedes cambiar la firma de tus cálculos y si envasas sus excepciones (como se muestra en publicaciones anteriores) o las reescribes para devolver null en su lugar, tu cadena de código se vuelve cada vez más breve y fácil debe decir:

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

Usé los siguientes reemplazos para sus funciones, lo que resulta en el valor 40.40 en 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! */}

Me doy cuenta de que esta solución no siempre será aplicable, pero usted planteó una pregunta muy interesante y creo, a pesar de que el hilo es relativamente antiguo, que este es un patrón que vale la pena considerar cuando se puede hacer las enmiendas.

 4
Author: Abel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-18 17:39:10

Dado que los métodos de cálculo tienen la misma firma sin parámetros, puede registrarlos en una lista, e iterar a través de esa lista y ejecutar los métodos. Probablemente sería incluso mejor para usted usar Func<double> que significa "una función que devuelve un resultado de tipo 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
Author: Marcin Seredynski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 16:19:28

Puede utilizar una Tarea/ContinueWith, y comprobar la excepción. Aquí hay un buen método de extensión para ayudar a que sea bonito:

    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
Author: Dax Fohl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-04 02:02:48

Si el tipo real de la excepción lanzada no importa, puede usar un bloque catch sin tipo:

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
Author: Jacob Krall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-18 17:46:47
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;
        }
    }
}

Y úsalo así:

using Utility;

...

TryHelper.TryUntilSuccessful(
    () =>
    {
        /* some code */
    },
    () =>
    {
        /* more code */
    },
    calc1,
    calc2,
    calc3,
    () =>
    {
        throw NotImplementedException();
    },
    ...
);
 1
Author: Ryan Lester,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-01 03:27:59

Parece que la intención de la OP era encontrar un buen patrón para resolver su problema y resolver el problema actual con el que estaba luchando en ese momento.

OP: "Podría envolver cada cálculo en un método helper que devuelve null en caso de fallo, y luego solo use el operador ??, pero ¿hay una manera de hacer esto más generalmente (es decir, sin tener que escribir un método de ayuda para cada método que quiero usar)? He pensado en escribir una estática método que utiliza genéricos que envuelve cualquier dado método en un try / catch y devuelve null en caso de fallo, pero no estoy seguro de cómo lo haría. Alguna idea?"

Vi muchas cosas buenas patrones que evitan los bloques anidados try catch, publicado en este feed, pero no encontró una solución al problema que se cita anteriormente. Entonces, aquí está la solución:

Como OP mencionado anteriormente, quería hacer un objeto wrapperque devuelve null el fracaso. Yo lo llamaría un pod (Pod a prueba de excepciones ).

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;

Pero qué pasa si quieres crear un pod seguro para un Tipo de referencia result devuelto por funciones/métodos 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}";
}

Por lo tanto, es posible que note que no hay necesidad "para escribir un método de ayuda para cada método que desee utilizar".

El dos tipos de vainas (para {[5] s}y ReferenceTypeResults) son basta .


Aquí está el código de SafePod. Pero no es un contenedor. En su lugar, crea un envoltorio delegado seguro para excepciones tanto para ValueTypeResult como para ReferenceTypeResult.

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

Así es como se puede aprovechar el operador de coalescencia nula ?? combinado con el poder de ciudadanos de primera clase entidades (delegate s).

 1
Author: AndreyWD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-24 14:28:54

Tiene razón al envolver cada cálculo, pero debe envolver de acuerdo con el principio tell-don't-ask.

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

Las operaciones son simples, y pueden cambiar su comportamiento de forma independiente. Y no importa por qué fallan. Como prueba podrías implementar calc1DefaultingToCalc2 como:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}
 0
Author: raisercostin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-18 23:58:43

Parece que sus cálculos tienen más información válida para devolver que solo el cálculo en sí. Tal vez tendría más sentido para ellos hacer su propio manejo de excepciones y devolver una clase "results" que contenga información de error, información de valor, etc. Piensa como lo hace la clase AsyncResult siguiendo el patrón async. A continuación, puede evaluar el resultado real del cálculo. Usted puede racionalizar esto pensando en términos que si un cálculo falla, que es justo como informativo como si pasara. Por lo tanto, una excepción es una información, no un "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

...

Por supuesto, me pregunto más sobre la arquitectura general que está utilizando para hacer estos cálculos.

 -1
Author: Emoire,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 17:45:32

Qué pasa con el seguimiento de las acciones que haces...

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 );
}
 -3
Author: Orn Kristjansson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-17 16:11:32