Шаблон, чтобы избежать вложенных попробовать ловить блоки? - PullRequest
112 голосов
/ 17 октября 2011

Рассмотрим ситуацию, когда у меня есть три (или более) способа выполнения вычислений, каждый из которых может дать сбой за исключением. Чтобы попытаться выполнить каждый расчет, пока мы не найдем тот, который был успешным, я делал следующее:

double val;

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

Есть ли какой-либо принятый шаблон, который добивается этого более хорошим способом? Конечно, я мог бы обернуть каждое вычисление во вспомогательный метод, который возвращает ноль при сбое, а затем просто использовать оператор ??, но есть ли способ сделать это более широко (т.е. без необходимости писать вспомогательный метод для каждого метода I хотите использовать)? Я думал о написании статического метода с использованием обобщений, который оборачивает любой данный метод в try / catch и возвращает null при сбое, но я не уверен, как бы я поступил об этом. Есть идеи?

Ответы [ 16 ]

126 голосов
/ 17 октября 2011

Насколько это возможно, не используйте исключения для потока управления или исключительных обстоятельств.

Но чтобы ответить на ваш вопрос напрямую (при условии, что все типы исключений одинаковы):

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

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

throw new NoCalcsWorkedException();
37 голосов
/ 17 октября 2011

Просто чтобы предложить альтернативу "нестандартно", как насчет рекурсивной функции ...

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

ПРИМЕЧАНИЕ: я ни в коем случае не говорю, что это лучший способ выполнить работу, просто другой способ

37 голосов
/ 17 октября 2011

Вы можете сгладить вложение, поместив его в метод, подобный этому:

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 заключается в существовании трех разных методов, которые делают одно и то же (с точки зрения вызывающего), но выдают разные, не связанные исключения.

Предполагается, что три исключения не связаны. Если они все имеют общий базовый класс, было бы лучше использовать цикл с одним блоком перехвата, как предложила Ани.

20 голосов
/ 17 октября 2011

Старайтесь не контролировать логику на основе исключений;Также обратите внимание, что исключения должны создаваться только в исключительных случаях.Расчеты в большинстве случаев не должны вызывать исключения, если они не обращаются к внешним ресурсам или не анализируют строки или что-то ещеВ любом случае в худшем случае следуйте стилю 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;
        }
9 голосов
/ 18 октября 2011

Это похоже на работу для ... МОНАДЫ!В частности, монада Может быть.Начните с монады 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 голосов
/ 17 октября 2011

Создайте список делегатов для ваших функций вычисления и затем выполните цикл 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?

Это должно дать вам общее представление о том, как это сделать (т.е. это не является точнымраствор).

7 голосов
/ 17 октября 2011

Другая версия подхода 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();
    }
4 голосов
/ 18 октября 2011

В Perl вы можете сделать foo() or bar(), который выполнит bar(), если foo() не удастся. В C # мы не видим эту конструкцию «если не получится, тогда», но есть оператор, который мы можем использовать для этой цели: оператор null-coalesce ??, который продолжается, только если первая часть равна нулю.

Если вы можете изменить сигнатуру своих вычислений и если вы либо заключите их исключения (как показано в предыдущих сообщениях), либо перепишете их, чтобы вернуть null, ваша цепочка кодов будет становиться все более краткой и все еще легко читаемой:

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

Я использовал следующие замены для ваших функций, что приводит к значению 40.40 в 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! */}

Я понимаю, что это решение не всегда будет применимо, но вы задали очень интересный вопрос, и я верю, что, хотя тема довольно старая, этот шаблон стоит рассмотреть, когда можно вносить исправления.

3 голосов
/ 04 февраля 2012

Вы можете использовать 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 голосов
/ 17 октября 2011

Учитывая, что методы расчета имеют одну и ту же сигнатуру без параметров, вы можете зарегистрировать их в списке, выполнить итерацию по этому списку и выполнить методы. Возможно, было бы еще лучше использовать Func<double>, что означает «функция, которая возвращает результат типа 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);
   }
}
...