Wzór, aby uniknąć zagnieżdżonych bloków try catch?

114

Rozważ sytuację, w której mam trzy (lub więcej) sposoby wykonywania obliczeń, z których każdy może zawieść z wyjątkiem. Aby spróbować każdego obliczenia, aż znajdziemy taki, który się powiedzie, wykonałem następujące czynności:

double val;

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

Czy jest jakiś przyjęty wzorzec, który pozwala osiągnąć to w lepszy sposób? Oczywiście mógłbym zawinąć każde obliczenie w metodę pomocniczą, która zwraca wartość null w przypadku niepowodzenia, a następnie po prostu użyć ??operatora, ale czy istnieje sposób na zrobienie tego bardziej ogólnie (tj. Bez konieczności pisania metody pomocniczej dla każdej metody, której chcę użyć )? Myślałem o napisaniu metody statycznej przy użyciu typów generycznych, która zawija dowolną metodę w try / catch i zwraca wartość null w przypadku niepowodzenia, ale nie jestem pewien, jak bym to zrobił. Jakieś pomysły?

jjoelson
źródło
Czy możesz podać szczegóły dotyczące obliczeń?
James Johnson,
2
Są to po prostu różne metody rozwiązywania / aproksymacji PDE. Pochodzą z biblioteki innej firmy, więc nie mogę ich zmienić, aby zwracały kody błędów lub null. Najlepsze, co mogłem zrobić, to owinąć każdą z osobna metodą.
jjoelson
Czy metody Calc są częścią twojego projektu (zamiast biblioteki innej firmy)? Jeśli tak, możesz wyciągnąć logikę, która generuje wyjątki i użyć jej, aby zdecydować, która metoda obliczeniowa ma zostać wywołana.
Chris,
1
Jest inny przypadek użycia tego, z którym spotkałem się w Javie - muszę przeanalizować a Stringdo Dateusing SimpleDateFormat.parsei muszę wypróbować kilka różnych formatów w kolejności, przechodząc do następnego, gdy rzuca wyjątek.
Miserable Variable

Odpowiedzi:

125

O ile to możliwe, nie używaj wyjątków dla przepływu kontroli lub nietypowych okoliczności.

Ale żeby odpowiedzieć bezpośrednio na twoje pytanie (zakładając, że wszystkie typy wyjątków są takie same):

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

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

throw new NoCalcsWorkedException();
Ani
źródło
15
Zakłada się, że Calc1Exception, Calc2Exceptioni Calc3Exceptionmają wspólną klasę bazową.
Wyzard
3
Na dodatek zakłada wspólny podpis - który nie jest tak odległy. Dobra odpowiedź.
TomTom
1
Dodałem również continuew bloku catch i breakpo bloku catch, aby pętla kończyła się, gdy obliczenia działają (dzięki Lirikowi za ten bit)
jjoelson
6
+1 tylko dlatego, że mówi „Nie używaj wyjątków dla przepływu sterowania”, chociaż użyłbym „NIGDY NIGDY” zamiast „O ile to możliwe”.
Bill K
1
@jjoelson: breakOświadczenie następujące calc();w try(i żadne continueoświadczenie) może być trochę bardziej idiomatyczne.
Adam Robinson
38

Aby zaoferować alternatywę „poza polem”, a co powiesz na funkcję rekurencyjną ...

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

UWAGA: w żadnym wypadku nie twierdzę, że jest to najlepszy sposób wykonania pracy, po prostu inny sposób

musefan
źródło
6
Musiałem raz zaimplementować „On Error Resume Next” w języku, a wygenerowany przeze mnie kod bardzo wyglądał tak.
Jacob Krall
4
Nigdy nie używaj instrukcji switch do tworzenia pętli for.
Jeff Ferland,
3
Nie da się utrzymać instrukcji przełączającej do zapętlania
Mohamed Abed,
1
Wiem, że moja odpowiedź nie jest najbardziej wydajnym kodem, ale z drugiej strony używanie bloków try / catch do tego rodzaju rzeczy i tak nie jest najlepszym sposobem. Niestety, OP korzysta z biblioteki innej firmy i musi zrobić wszystko, co w jej mocy, aby zapewnić sukces. Idealnie byłoby, gdyby dane wejściowe zostały najpierw zatwierdzone i wybrana poprawna funkcja obliczeniowa, aby upewnić się, że nie zawiedzie - oczywiście, możesz następnie umieścić to wszystko w próbie / złapaniu, aby być bezpiecznym;)
musefan
1
return DoCalc(c++)jest równoważne return DoCalc(c)- wartość po zwiększeniu nie zostanie przekazana głębiej. Aby to działało (i wprowadzało więcej niejasności), mogłoby być bardziej podobne return DoCalc((c++,c)).
Artur Czajka
37

Możesz spłaszczyć zagnieżdżenie, wprowadzając je do metody takiej jak ta:

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

Ale podejrzewam, że prawdziwym problemem projektowym jest istnienie trzech różnych metod, które zasadniczo robią to samo (z perspektywy dzwoniącego), ale rzucają różne, niepowiązane wyjątki.

Zakłada się, że te trzy wyjątki nie ze sobą powiązane. Jeśli wszystkie mają wspólną klasę bazową, lepiej byłoby użyć pętli z jednym blokiem catch, jak zasugerowała Ani.

Wyzard
źródło
1
+1: To najczystsze, najbardziej rozsądne rozwiązanie problemu. Inne rozwiązania, które tu widzę, po prostu starają się być urocze, IMO. Jak powiedział OP, nie napisał API, więc utknął z wyrzucanymi wyjątkami.
Nate CK
19

Staraj się nie kontrolować logiki opartej na wyjątkach; należy również pamiętać, że wyjątki powinny być rzucane tylko w wyjątkowych przypadkach. Obliczenia w większości przypadków nie powinny zgłaszać wyjątków, chyba że uzyskują dostęp do zasobów zewnętrznych lub analizują ciągi lub coś takiego. W każdym razie w najgorszym przypadku postępuj zgodnie ze stylem TryMethod (takim jak TryParse ()), aby hermetyzować logikę wyjątków i sprawić, by przepływ sterowania był utrzymywany i czysty:

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

Inna odmiana wykorzystująca wzorzec Try i łącząca listę metod zamiast zagnieżdżonych, jeśli:

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;

a jeśli foreach jest trochę skomplikowane, możesz to wyjaśnić:

        foreach (var tryCalc in tryCalcs)
        {
            if (tryCalc(out calcOutput)) break;
        }
Mohamed Abed
źródło
Szczerze mówiąc, myślę, że to po prostu powoduje niepotrzebne abstrakcje. To nie jest straszne rozwiązanie, ale w większości przypadków nie użyłbym tego.
user606723
Jeśli nie zależy Ci na typie wyjątku, a chcesz po prostu obsłużyć kod warunkowy ... dlatego zdecydowanie lepiej pod względem abstrakcji i łatwości utrzymania jest przekonwertowanie go na metodę warunkową z zwracaniem bez względu na to, czy się powiedzie, czy nie, w ten sposób ukrywasz niechlujną składnię obsługi wyjątków za pomocą jasnej, opisowej metody ... wtedy Twój kod poradzi sobie z nią tak, jak jest to zwykła metoda warunkowa.
Mohamed Abed
Znam punkty i są one ważne. Jednak przy stosowaniu tego typu abstrakcji (ukrywania bałaganu / złożoności) prawie wszędzie staje się to śmieszne, a zrozumienie tego, czym jest oprogramowanie, staje się znacznie trudniejsze. Jak powiedziałem, nie jest to straszne rozwiązanie, ale nie użyłbym go lekko.
user606723
9

Utwórz listę delegatów do funkcji obliczeniowych, a następnie korzystaj z pętli while, aby przechodzić między nimi:

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?

To powinno dać ci ogólne pojęcie, jak to zrobić (tj. Nie jest to dokładne rozwiązanie).

Kiril
źródło
1
Z wyjątkiem tego, że normalnie nigdy nie powinieneś Exceptionwtedy łapać . ;)
DeCaf
@DeCaf, jak powiedziałem: „powinno dać ci ogólne pojęcie, jak to zrobić (tj. Nie jest to dokładne rozwiązanie)”. Tak więc PO może wychwycić każdy odpowiedni wyjątek ... nie ma potrzeby przechwytywania generycznego Exception.
Kiril,
Tak, przepraszam, po prostu czułem potrzebę, żeby to tam wydobyć.
DeCaf
1
@DeCaf, to ważne wyjaśnienie dla tych, którzy mogą nie być zaznajomieni z najlepszymi praktykami. Dzięki :)
Kiril
9

To wygląda na pracę dla ... MONADS! W szczególności monada Może. Zacznij od monady Może, jak opisano tutaj . Następnie dodaj kilka metod rozszerzających. Napisałem te metody rozszerzające specjalnie dla problemu, jak go opisałeś. Zaletą monad jest to, że możesz napisać dokładne metody rozszerzenia potrzebne w twojej sytuacji.

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

Użyj go w ten sposób:

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

Jeśli często wykonujesz tego rodzaju obliczenia, być może monada powinna zmniejszyć ilość gotowego kodu, który musisz napisać, jednocześnie zwiększając czytelność twojego kodu.

fre0n
źródło
2
Uwielbiam to rozwiązanie. Jednak jest to dość nieprzejrzyste dla każdego, kto wcześniej nie miał kontaktu z monadami, co oznacza, że ​​jest to bardzo dalekie od idiomatycznego w języku c #. Nie chciałbym, aby któryś z moich współpracowników nauczył się monad tylko po to, aby zmodyfikować ten jeden głupi fragment kodu w przyszłości. Jest to jednak świetne do wykorzystania w przyszłości.
jjoelson
1
+1 za poczucie humoru, za napisanie jak najbardziej rozwlekłego i rozwlekłego rozwiązania tego problemu, a następnie stwierdzenie, że „zmniejszy to ilość gotowego kodu, który musisz napisać, jednocześnie zwiększając czytelność kodu”.
Nate CK
1
Hej, nie narzekamy na ogromne ilości kodu czającego się w System.Linq i szczęśliwie używamy tych monad przez cały dzień. Myślę, że @ fre0n oznacza po prostu, że jeśli chcesz umieścić monadę Może w swoim zestawie narzędzi, tego rodzaju łańcuchowe oceny staną się łatwiejsze do spojrzenia i rozważenia. Istnieje kilka łatwych do uchwycenia implementacji.
Sebastian Good
To, że używa Maybe, nie czyni tego monadycznym rozwiązaniem; wykorzystuje zero monadycznych właściwości, Maybewięc równie dobrze może po prostu użyć null. Poza tym stosowane „monadicly” to byłby odwrotny z Maybe. Prawdziwe rozwiązanie monadyczne wymagałoby użycia monady stanu, która zachowuje pierwszą nie-wyjątkową wartość jako swój stan, ale byłoby to przesadą, gdyby działała normalna ocena łańcuchowa.
Dax Fohl
7

Inna wersja metody try . Ten pozwala na wyjątki wpisane, ponieważ istnieje typ wyjątku dla każdego obliczenia:

    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();
    }
Stefan
źródło
W rzeczywistości możesz uniknąć zagnieżdżonych ifs, używając &&zamiast tego między każdym warunkiem.
DeCaf
4

W Perlu możesz to zrobić foo() or bar(), co zostanie wykonane, bar()jeśli się foo()nie powiedzie. W C # nie widzimy tej konstrukcji „if fail, then”, ale istnieje operator, którego możemy użyć do tego celu: operator null-coalesce ??, który jest kontynuowany tylko wtedy, gdy pierwsza część ma wartość null.

Jeśli możesz zmienić podpis swoich obliczeń i jeśli zawiniesz ich wyjątki (jak pokazano w poprzednich postach) lub przepisasz je, aby nullzamiast tego zwracały, twój łańcuch kodu staje się coraz krótszy i nadal łatwy do odczytania:

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

Kiedyś następujące zamienniki dla swoich funkcji, co prowadzi do wartości 40.40w 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! */}

Zdaję sobie sprawę, że to rozwiązanie nie zawsze będzie miało zastosowanie, ale zadałeś bardzo ciekawe pytanie i uważam, mimo że wątek jest stosunkowo stary, że jest to wzorzec, który warto rozważyć, kiedy można to naprawić.

Abel
źródło
1
Chcę tylko powiedzieć „Dziękuję”. Próbowałem wdrożyć to, o czym mówiłeś . Mam nadzieję, że dobrze to zrozumiałem.
AlexMelw
3

Biorąc pod uwagę, że metody obliczeniowe mają ten sam podpis bezparametrowy, można zarejestrować je na liście, iterować po tej liście i wykonywać metody. Prawdopodobnie byłoby jeszcze lepiej, gdybyś użył Func<double>znaczenia „funkcja zwracająca wynik typu 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);
   }
}
Marcin Seredynski
źródło
3

Możesz użyć Task / ContinueWith i sprawdzić wyjątek. Oto fajna metoda rozszerzenia, dzięki której jest ładna:

    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);
    }
}
Dax Fohl
źródło
1

Jeśli rzeczywisty typ zgłoszonego wyjątku nie ma znaczenia, możesz po prostu użyć bloku catch bez typu:

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();
Jacob Krall
źródło
Czy to nie zawsze wywołuje każdą funkcję na liście? Może chcieć rzucić (gra słów nie jest przeznaczona) w coś takiego jak if(succeeded) { break; }po złapaniu.
CVn
1
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;
        }
    }
}

I używaj go w ten sposób:

using Utility;

...

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

Wydaje się, że intencją PO było znalezienie dobrego schematu rozwiązania jego problemu i rozwiązania obecnego problemu, z którym w tym momencie się borykał.

OP: „Mógłbym zawinąć każde obliczenie w metodę pomocniczą, która zwraca wartość null w przypadku niepowodzenia, a następnie po prostu użyć ??operatora, ale czy istnieje sposób na zrobienie tego bardziej ogólnie (tj. Bez konieczności pisania metody pomocniczej dla każdej metody, którą chcę Używam)? Myślałem o napisaniu statycznej metody przy użyciu typów ogólnych, która zawija dowolną metodę w try / catch i zwraca wartość null w przypadku niepowodzenia, ale nie jestem pewien, jak bym to zrobił. Jakieś pomysły? ”

Widziałem wiele dobrych wzorców unikających zagnieżdżonych bloków try catch , opublikowanych w tym kanale, ale nie znalazłem rozwiązania problemu, który jest cytowany powyżej. Oto rozwiązanie:

Jak wspomniał powyżej OP, chciał stworzyć obiekt opakowujący, który powraca nullw przypadku niepowodzenia . Nazwałbym to kapsułą ( kapsuła bezpieczna dla wyjątków ).

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;

Ale co, jeśli chcesz utworzyć bezpieczny pod dla wyniku typu odwołania zwracanego przez funkcje / metody 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}";
}

Możesz więc zauważyć, że nie ma potrzeby „pisania metody pomocniczej dla każdej metody, której chcesz użyć” .

Te dwa rodzaje strąków (dla ValueTypeResultS i ReferenceTypeResultS) są na tyle .


Oto kod SafePod. Nie jest to jednak pojemnik. Zamiast tego tworzy bezpieczne opakowanie delegata dla obu ValueTypeResults i 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;
    }
}

W ten sposób można wykorzystać operator koalescencji zerowej w ??połączeniu z siłą pierwszorzędnych podmiotów obywatelskichdelegate .

AlexMelw
źródło
0

Masz rację co do zawijania wszystkich obliczeń, ale powinieneś zawijać je zgodnie z zasadą powiedz-nie-pytaj.

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

Operacje są proste i mogą niezależnie zmieniać ich zachowanie. I nie ma znaczenia, dlaczego upadają. Jako dowód możesz zaimplementować calc1DefaultingToCalc2 jako:

double calc1DefaultingToCalc2(){
    try { 
        val = calc2(); 
        if(specialValue(val)){
            val = calc2DefaultingToCalc3WithConvertedException()
        }
    }
    catch (Calc1Exception e1)
    {
        //defaulting to simpler method
        return calc2defaultingToCalc3WithConvertedException();
    }
}
raisercostin
źródło
-1

Wygląda na to, że obliczenia zawierają więcej ważnych informacji do zwrócenia niż same obliczenia. Być może bardziej sensowne byłoby dla nich wykonanie własnej obsługi wyjątków i zwrócenie klasy „wyników”, która zawiera informacje o błędach, wartościach itp. Pomyśl tak, jak klasa AsyncResult postępuje według wzorca asynchronicznego. Następnie możesz ocenić rzeczywisty wynik obliczeń. Możesz to zracjonalizować, myśląc w kategoriach, że jeśli obliczenia się nie powiodą, są tak samo informacyjne, jak gdyby przeszły. Dlatego wyjątek to informacja, a nie „błąd”.

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

...

Oczywiście bardziej się zastanawiam nad ogólną architekturą, której używasz do wykonywania tych obliczeń.

Emoire
źródło
To jest w porządku - podobnie do tego, co sugerował OP, jeśli chodzi o opakowane obliczenia. Wolałbym tylko coś takiego while (!calcResult.HasValue) nextCalcResult(), zamiast listy Calc1, Calc2, Calc3 itd.
Kirk Broadhurst
-3

A co ze śledzeniem działań, które wykonujesz ...

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 );
}
Orn Kristjansson
źródło
4
Jak to pomaga? jeśli calc1 () zawiedzie, cals2 nigdy nie zostanie wykonane!
DeCaf
to nie rozwiązuje problemu. Wykonaj calc1 tylko wtedy, gdy calc2 nie powiedzie się, wykonuj calc3 tylko wtedy, gdy calc1 && calc2 zawiedzie.
Jason
+1 orn. Tym się właśnie zajmuję. Muszę tylko zakodować jeden haczyk, wiadomość wysłaną do mnie ( trackw tym przypadku) i wiem dokładnie, który segment w moim kodzie spowodował awarię bloku. Może powinieneś był opracować, aby poinformować członków, takich jak DeCaf, że trackwiadomość jest wysyłana do niestandardowej procedury obsługi błędów, która umożliwia debugowanie kodu. Wygląda na to, że nie rozumiał twojej logiki.
jp2code
Cóż, @DeCaf jest poprawne, mój segment kodu nie wykonuje następnej funkcji, o którą prosił jjoelson, moje rozwiązanie jest niewykonalne
Orn Kristjansson