Шаблон, чтобы избежать вложенных блоков catch catch?
Рассмотрим ситуацию, когда у меня есть три (или более) способа выполнения вычисления, каждый из которых может сбой с исключением. Чтобы выполнить каждый расчет до тех пор, пока мы не найдем тот, который преуспевает, я делаю следующее:
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 при ошибке, но я не уверен, как бы я это сделал. Любые идеи?
Ответы
Ответ 1
Насколько это возможно, не используйте исключения для потока управления или непредвиденных обстоятельств.
Но ответ на ваш вопрос напрямую (если все типы исключений совпадают):
Func<double>[] calcs = { calc1, calc2, calc3 };
foreach(var calc in calcs)
{
try { return calc(); }
catch (CalcException){ }
}
throw new NoCalcsWorkedException();
Ответ 2
Вы можете сгладить вложенность, поместив его в такой метод:
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();
}
Но я подозреваю, что проблема с реальным дизайном заключается в существовании трех разных методов, которые делают практически одно и то же (с точки зрения вызывающего абонента), но создают разные, не связанные с ними исключения.
Это предполагает, что три исключения не связаны. Если все они имеют общий базовый класс, было бы лучше использовать цикл с одним блоком catch, как предположил Ани.
Ответ 3
Просто, чтобы предложить альтернативу "вне коробки", как насчет рекурсивной функции...
//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);
}
}
ПРИМЕЧАНИЕ. Я ни в коем случае не говорю, что это лучший способ выполнить работу, просто по-другому.
Ответ 4
Попытайтесь не управлять логикой на основе исключений; обратите внимание также, что исключения следует бросать только в исключительных случаях. Вычисления в большинстве случаев не должны генерировать исключения, если они не обращаются к внешним ресурсам или синтаксическим строкам или чем-то еще. В любом случае в худшем случае следуйте стилю 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;
}
Ответ 5
Создайте список делегатов для ваших функций вычисления, а затем запустите цикл 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?
Это должно дать вам общее представление о том, как это сделать (т.е. это не точное решение).
Ответ 6
Это похоже на работу для... 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 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));
}
Если вы часто делаете подобные вычисления, возможно, монада должна уменьшить количество кода шаблона, который вы должны писать, одновременно повышая читабельность вашего кода.
Ответ 7
Другая версия метода метода 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();
}
Ответ 8
Учитывая, что методы расчета имеют одинаковую безпараметрическую подпись, вы можете зарегистрировать их в списке и выполнить итерацию по этому списку и выполнить методы. Вероятно, для вас было бы лучше использовать 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);
}
}
Ответ 9
В Perl вы можете сделать foo() or bar()
, который выполнит bar()
, если foo()
завершится сбой. В С# мы не видим этого "if fail, then" construct, но есть оператор, который мы можем использовать для этой цели: оператор null-coalesce ??
, который продолжается только в том случае, если первая часть равна null.
Если вы можете изменить подпись своих вычислений и если вы либо завершите их исключения (как показано в предыдущих сообщениях), либо перепишите их для возврата 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! */}
Я понимаю, что это решение не всегда применимо, но вы задали очень интересный вопрос, и я считаю, что хотя поток относительно старый, это шаблон, который стоит рассмотреть, когда вы можете внести исправления.
Ответ 10
Вы можете использовать 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);
}
}
Ответ 11
Если фактический тип брошенного исключения не имеет значения, вы можете просто использовать безликий блок 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();
Ответ 12
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();
},
...
);
Ответ 13
Вы правы в том, чтобы обернуть каждый расчет, но вы должны обернуть по принципу 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();
}
}
Операции просты и могут изменять свое поведение независимо. И не имеет значения, почему они по умолчанию.
В качестве доказательства вы можете реализовать calc1DefaultingToCalc2 как:
double calc1DefaultingToCalc2(){
try {
val = calc2();
if(specialValue(val)){
val = calc2DefaultingToCalc3WithConvertedException()
}
}
catch (Calc1Exception e1)
{
//defaulting to simpler method
return calc2defaultingToCalc3WithConvertedException();
}
}
Ответ 14
Похоже, ваши расчеты возвращают более достоверную информацию, чем просто расчет. Возможно, для них было бы более целесообразно выполнять собственную обработку исключений и возвращать класс "результатов", содержащий информацию об ошибках, информацию о значении и т.д. Подумайте, как класс AsyncResult выполняет асинхронный шаблон. Затем вы можете оценить реальный результат расчета. Вы можете рационализировать это, думая в терминах, что если расчет не удастся, это так же информативно, как если бы оно прошло. Поэтому исключение представляет собой часть информации, а не "ошибку".
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
...
Конечно, мне интересно узнать больше об общей архитектуре, которую вы используете, чтобы выполнить эти вычисления.
Ответ 15
Как насчет отслеживания действий, которые вы делаете...
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 );
}