Czy istnieją uzasadnione powody, aby zwracać obiekty wyjątków zamiast je wyrzucać?

45

To pytanie ma na celu zastosowanie do dowolnego języka programowania OO, który obsługuje obsługę wyjątków; Używam C # tylko w celach ilustracyjnych.

Wyjątki są zwykle zgłaszane, gdy pojawia się problem, którego kod nie może natychmiast obsłużyć, a następnie przechwytywane w catchklauzuli w innym miejscu (zwykle zewnętrznej ramce stosu).

P: Czy istnieją uzasadnione sytuacje, w których wyjątki nie są zgłaszane i wychwytywane, ale po prostu wracają z metody, a następnie przekazywane jako obiekty błędów?

To pytanie pojawiło się dla mnie, ponieważ System.IObserver<T>.OnErrormetoda .NET 4 sugeruje właśnie to: wyjątki są przekazywane jako obiekty błędów.

Spójrzmy na inny scenariusz, sprawdzanie poprawności. Powiedzmy, że postępuję zgodnie z konwencjonalną mądrością i dlatego rozróżniam typ obiektu błędu IValidationErrorod osobnego typu wyjątku, ValidationExceptionktóry służy do zgłaszania nieoczekiwanych błędów:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(Przestrzeń System.Component.DataAnnotationsnazw robi coś całkiem podobnego).

Te typy można zastosować w następujący sposób:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Zastanawiam się, czy nie mógłbym uprościć powyższego:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

P: Jakie są zalety i wady tych dwóch różnych podejść?

stakx
źródło
Jeśli szukasz dwóch osobnych odpowiedzi, powinieneś zadać dwa osobne pytania. Połączenie ich sprawia, że ​​o wiele trudniej jest odpowiedzieć.
Bobson
2
@ Bobson: Te dwa pytania uznaję za komplementarne: pierwsze pytanie ma na celu ogólne zdefiniowanie ogólnego kontekstu problemu, drugie pytanie o konkretne informacje, które powinny umożliwić czytelnikom wyciągnięcie własnych wniosków. Odpowiedź nie musi dawać osobnych, jednoznacznych odpowiedzi na oba pytania; odpowiedzi „pomiędzy” są równie mile widziane.
stakx
5
Nie mam pojęcia: co jest powodem wyprowadzenia ValidationError z wyjątku, jeśli nie zamierzasz go wyrzucić?
pdr
@pdr: Czy masz na myśli AggregateExceptionzamiast tego rzucanie ? Słuszna uwaga.
stakx
3
Nie całkiem. Mam na myśli to, że jeśli zamierzasz to rzucić, użyj wyjątku. Jeśli chcesz go zwrócić, użyj obiektu błędu. Jeśli spodziewałeś się wyjątków, które są z natury błędami, złap je i umieść w swoim obiekcie błędu. To, czy którykolwiek z nich dopuszcza wiele błędów, jest całkowicie nieistotne.
pdr

Odpowiedzi:

31

Czy istnieją uzasadnione sytuacje, w których wyjątki nie są zgłaszane i wychwytywane, ale po prostu wracają z metody, a następnie przekazywane jako obiekty błędów?

Jeśli nigdy nie zostanie wyrzucony, nie jest to wyjątkiem. Jest to obiekt derivedz klasy Exception, chociaż nie jest zgodny z zachowaniem. Nazywanie tego Wyjątkiem jest w tym momencie czysto semantyką, ale nie widzę nic złego w tym, że go nie rzucam. Z mojego punktu widzenia nie rzucanie wyjątku jest dokładnie tym samym, co funkcja wewnętrzna przechwytująca go i uniemożliwiająca jego propagację.

Czy funkcja może zwrócić obiekty wyjątków?

Absolutnie. Oto krótka lista przykładów, w których może to być odpowiednie:

  • Fabryka wyjątków.
  • Obiekt kontekstowy, który zgłasza, czy wystąpił poprzedni błąd jako wyjątek gotowy do użycia.
  • Funkcja, która utrzymuje poprzednio wychwycony wyjątek.
  • Interfejs API innej firmy, który tworzy wyjątek typu wewnętrznego.

Czy nie rzuca się źle?

Zgłoszenie wyjątku przypomina: „Idź do więzienia. Nie przechodź Idź!” w grze planszowej Monopoly. Mówi kompilatorowi, aby przeskoczył cały kod źródłowy aż do połowu bez wykonywania żadnego z tego kodu źródłowego. Nie ma to nic wspólnego z błędami, zgłaszaniem ani zatrzymywaniem błędów. Lubię myśleć o zgłoszeniu wyjątku jako instrukcji „super return” dla funkcji. Zwraca wykonanie kodu gdzieś znacznie wyżej niż pierwotny program wywołujący.

Ważne jest, aby zrozumieć, że prawdziwą wartością wyjątków jest try/catchwzorzec, a nie instancja obiektu wyjątku. Obiekt wyjątku jest tylko komunikatem stanu.

W twoim pytaniu wydajesz się mylić użycie tych dwóch rzeczy: przeskoczenie do modułu obsługi wyjątku i stan błędu, który reprezentuje wyjątek. To, że przyjąłeś stan błędu i umieściłeś go w wyjątku, nie oznacza, że ​​podążasz za try/catchwzorem lub jego zaletami.

Reactgular
źródło
4
„Jeśli nigdy nie zostanie wyrzucony, nie będzie to wyjątkiem. Jest to obiekt wywodzący się z Exceptionklasy, ale nie podążający za zachowaniem”. - I o to właśnie chodzi: czy uzasadnione jest użycie Exceptionobiektu pochodzącego z wyprowadzenia w sytuacji, gdy nie zostanie on w ogóle wyrzucony ani przez producenta obiektu, ani przez żadną metodę wywoływania; gdzie służy jedynie jako „komunikat stanu” przekazywany bez przepływu kontroli „super powrotu”? Czy też tak bardzo naruszam niewypowiedzianą umowę try/ catch(Exception)umowę, że nigdy nie powinienem tego robić, a zamiast tego używam osobnego, innego niż Exceptiontyp?
stakx
10
Programiści @stakx mają i będą robić rzeczy, które są mylące dla innych programistów, ale nie technicznie niepoprawne. Dlatego powiedziałem, że to semantyka. Prawna odpowiedź brzmi: Nonie zrywasz żadnych umów. popular opinionByłoby to, że nie powinno się tego robić. Programowanie jest pełne przykładów, w których kod źródłowy nie robi nic złego, ale wszyscy go nie lubią. Kod źródłowy powinien być zawsze napisany, aby było jasne, o co chodzi. Czy naprawdę pomagasz innemu programistowi, wykorzystując w ten sposób wyjątek? Jeśli tak, to zrób to. W przeciwnym razie nie zdziw się, jeśli się nie spodoba.
Reactgular,
@cgTag Używanie wbudowanych bloków kursywy powoduje, że mózg mnie boli ..
Frayt,
51

Zwracanie wyjątków zamiast ich zgłaszania może mieć sens semantyczny, gdy masz metodę pomocniczą do analizy sytuacji i zwracania odpowiedniego wyjątku, który jest następnie zgłaszany przez wywołującego (można nazwać to „fabryką wyjątków”). Zgłoszenie wyjątku w tej funkcji analizatora błędów oznaczałoby, że coś poszło nie tak podczas samej analizy, a zwrócenie wyjątku oznacza, że ​​rodzaj błędu został pomyślnie przeanalizowany.

Jednym z możliwych przypadków użycia może być funkcja, która zamienia kody odpowiedzi HTTP w wyjątki:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Zauważ, że rzucanie e środki różnicą, że metoda została zastosowana błędne lub wystąpił błąd wewnętrzny, natomiast wracając e środki różnicą, że kod błędu został zidentyfikowany pomyślnie.

Philipp
źródło
Pomaga to również kompilatorowi zrozumieć przepływ kodu: jeśli metoda nigdy nie zwróci poprawnie (ponieważ zgłasza „pożądany” wyjątek), a kompilator nie wie o tym, może być czasem potrzebne zbędne returninstrukcje, aby kompilator przestał narzekać.
Joachim Sauer
@JoachimSauer Tak. Niejednokrotnie chciałbym, aby dodali do C # zwrot typu „nigdy” - oznaczałoby to, że funkcja musi wyjść przez rzucanie, wstawienie w to błędu. Czasami masz kod, którego jedynym zadaniem jest wykrycie wyjątku.
Loren Pechtel,
2
@LorenPechtel Funkcja, która nigdy nie zwraca, nie jest funkcją. To terminator. Wywołanie takiej funkcji powoduje wyczyszczenie stosu wywołań. To jest podobne do dzwonienia System.Exit(). Kod po wywołaniu funkcji nie jest wykonywany. To nie brzmi jak dobry wzór dla funkcji.
Reactgular,
5
@MathewFoscarini Rozważ klasę, która ma wiele metod, które mogą popełniać błędy w podobny sposób. Chcesz zrzucić niektóre informacje o stanie jako część wyjątku, aby wskazać, co go przeżuwało, gdy boom. Potrzebujesz jednej kopii kodu do poprawiania z powodu DRY. To pozostawia albo wywołanie funkcji, która wykonuje poprawkę i użycie wyniku w rzucie, albo oznacza funkcję, która buduje upiększony tekst, a następnie go rzuca. Zazwyczaj uważam tego drugiego przełożonego.
Loren Pechtel,
1
@LorenPechtel ah, tak, ja też to zrobiłem. OK, rozumiem teraz.
Reactgular,
5

Koncepcyjnie, jeśli obiekt wyjątku jest oczekiwanym wynikiem operacji, to tak. Przypadki, o których mogę myśleć, zawsze wiążą się z pewnym rzutem:

  • Wariacje na temat wzorca „Try” (gdzie metoda zawiera drugą metodę zgłaszania wyjątków, ale przechwytuje wyjątek i zamiast tego zwraca wartość logiczną wskazującą powodzenie). Zamiast zwracać wartość logiczną można zwrócić zgłoszony wyjątek (zerowy, jeśli się powiedzie), umożliwiając użytkownikowi końcowemu więcej informacji, zachowując bezdotykowy wskaźnik sukcesu lub niepowodzenia.

  • Przetwarzanie błędu przepływu pracy. Podobnie w praktyce do enkapsulacji metody „próbuj wzorzec”, możesz mieć etap przepływu pracy wyodrębniony we wzorcu łańcucha poleceń. Jeśli łącze w łańcuchu zgłasza wyjątek, często łatwiej jest złapać ten wyjątek w obiekcie abstrakcyjnym, a następnie zwrócić go w wyniku wspomnianej operacji, aby silnik przepływu pracy i rzeczywisty kod działający jako krok przepływu pracy nie wymagały obszernej próby -chwytaj własną logikę.

KeithS
źródło
Nie sądzę, żeby widzieli jakieś godne uwagi osoby opowiadające się za taką odmianą wzoru „spróbuj”, ale „zalecane” podejście zwracania logiki wydaje się dość anemiczne. Pomyślałbym, że lepiej byłoby mieć OperationResultklasę abstrakcyjną z boolwłaściwością „Successful”, GetExceptionIfAnymetodą i AssertSuccessfulmetodą (która wyrzuciłaby wyjątek, GetExceptionIfAnyjeśli nie jest zerowy). Posiadanie GetExceptionIfAnymetody zamiast właściwości pozwoliłoby na użycie statycznych niezmiennych obiektów błędów w przypadkach, w których nie było wiele użytecznych do powiedzenia.
supercat
5

Powiedzmy, że zleciłeś wykonanie zadania w puli wątków. Jeśli to zadanie generuje wyjątek, jest to inny wątek, więc go nie złapiesz. Wątek, w którym został wykonany, po prostu umrzyj.

Teraz zastanów się, że coś (kod w tym zadaniu lub implementacja puli wątków) łapie ten wyjątek, zapisz go wraz z zadaniem i uważaj zadanie za ukończone (bez powodzenia). Teraz możesz zapytać, czy zadanie zostało zakończone (albo zapytać, czy zostało rzucone, czy może zostać ponownie wrzucone do wątku (lub lepiej, nowy wyjątek z oryginałem jako przyczyną)).

Jeśli robisz to ręcznie, możesz zauważyć, że tworzysz nowy wyjątek, rzucasz go i łapiesz, a następnie przechowujesz, rzucając, chwytając i reagując na różne wątki. Dlatego warto pominąć rzut i złapać, po prostu przechowywać i zakończyć zadanie, a następnie po prostu zapytać, czy jest wyjątek i zareagować na to. Ale może to prowadzić do bardziej skomplikowanego kodu, jeśli w tym samym miejscu mogą istnieć wyjątki.

PS: jest to napisane z doświadczeniem w Javie, gdzie informacje śledzenia stosu są tworzone, gdy tworzony jest wyjątek (w przeciwieństwie do C #, gdzie jest tworzony podczas rzucania). Podejście nie w przypadku wyjątków zgłaszanych w Javie będzie wolniejsze niż w języku C # (chyba że zostanie wstępnie przetworzone i ponownie użyte), ale będą dostępne informacje o śledzeniu stosu.

Ogólnie rzecz biorąc, unikałbym tworzenia wyjątku i nigdy go nie rzucałbym (chyba że optymalizacja wydajności po profilowaniu wskazuje to jako wąskie gardło). Przynajmniej w java, gdzie tworzenie wyjątków jest drogie (śledzenie stosu). W C # jest to możliwe, ale IMO jest zaskakujące i dlatego należy tego unikać.

użytkownik470365
źródło
Często zdarza się to również w przypadku obiektów nasłuchujących błędów. Kolejka wykona zadanie, złapie dowolny wyjątek, a następnie przekaże ten wyjątek odpowiedzialnemu odbiornikowi błędów. Zazwyczaj nie ma sensu zabijać wątku zadania, który w tym przypadku nie wie dużo o zadaniu. Ten rodzaj pomysłu jest również wbudowany w interfejsy API Java, w których jeden wątek może przechodzić wyjątki od innego: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek
1

To zależy od twojego projektu, zwykle nie zwracam wyjątków dzwoniącemu, raczej rzucam je, łapię i zostawiam. Zazwyczaj kod jest zapisywany tak, aby nie działał wcześnie i szybko. Na przykład rozważmy przypadek otwarcia pliku i przetworzenia go (jest to C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

W takim przypadku pomylilibyśmy się i przestaliśmy przetwarzać plik, gdy tylko napotkał zły rekord.

Ale powiedzmy, że wymaganie polega na tym, że chcemy kontynuować przetwarzanie pliku, nawet jeśli jeden lub więcej wierszy zawiera błąd. Kod może zacząć wyglądać mniej więcej tak:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Tak więc w tym przypadku zwracamy wyjątek z powrotem do programu wywołującego, chociaż prawdopodobnie nie zwróciłbym wyjątku z powrotem, ale zamiast tego jakiś obiekt błędu, ponieważ wyjątek został wychwycony i już rozwiązany. Nie jest to szkodliwe ze zwróceniem wyjątku z powrotem, ale inni wywołujący mogą ponownie wygenerować wyjątek, co może nie być pożądane, ponieważ obiekt wyjątku ma takie zachowanie. Jeśli chcesz, aby osoby dzwoniące miały możliwość ponownego wyrzucenia, a następnie zwróć wyjątek, w przeciwnym razie wyjmij informacje z obiektu wyjątku i zbuduj mniejszy obiekt o niewielkiej masie i zwróć go. Błąd szybki to zazwyczaj czystszy kod.

W twoim przykładzie sprawdzania poprawności prawdopodobnie nie odziedziczyłbym po klasie wyjątków ani nie wyrzucał wyjątków, ponieważ błędy sprawdzania poprawności mogą być dość powszechne. Zwrócenie obiektu byłoby mniej obciążeniem niż rzucanie wyjątku, jeśli 50% moich użytkowników nie wypełni poprawnie formularza przy pierwszej próbie.

Jon Raynor
źródło
1

P: Czy istnieją uzasadnione sytuacje, w których wyjątki nie są zgłaszane i wychwytywane, ale po prostu wracają z metody, a następnie przekazywane jako obiekty błędów?

Tak. Na przykład ostatnio spotkałem się z sytuacją, w której zauważyłem, że zgłoszone wyjątki w donocie usługi sieciowej ASMX zawierają element w wynikowym komunikacie SOAP, więc musiałem go wygenerować.

Kod ilustracyjny:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
Tom Tom
źródło
1

W większości języków (afaik) wyjątki zawierają dodatkowe dodatki. Zwykle migawka bieżącego śladu stosu jest przechowywana w obiekcie. Jest to przydatne do debugowania, ale może mieć również wpływ na pamięć.

Jak już powiedział @ThinkingMedia, naprawdę używasz wyjątku jako obiektu błędu.

W twoim przykładzie kodu wydaje się, że robisz to głównie w celu ponownego użycia kodu i uniknięcia kompozycji. Osobiście nie uważam, że to dobry powód, aby to zrobić.

Innym możliwym powodem może być oszukiwanie języka w celu uzyskania obiektu błędu ze śledzeniem stosu. Daje to dodatkowe informacje kontekstowe w kodzie obsługi błędów.

Z drugiej strony możemy założyć, że utrzymanie śladu stosu ma koszt pamięci. Na przykład, jeśli zaczniesz gdzieś zbierać te obiekty, możesz zauważyć nieprzyjemny wpływ na pamięć. Oczywiście w dużej mierze zależy to od tego, jak ślady stosu wyjątków są implementowane w odpowiednim języku / silniku.

Czy to dobry pomysł?

Do tej pory nie widziałem, aby był używany lub zalecany. Ale to niewiele znaczy.

Pytanie brzmi: czy śledzenie stosu jest naprawdę przydatne do obsługi błędów? Lub bardziej ogólnie, jakie informacje chcesz przekazać deweloperowi lub administratorowi?

Typowy wzorzec rzucania / próbowania / łapania wymaga pewnego rodzaju śledzenia stosu, ponieważ w przeciwnym razie nie masz pojęcia, skąd pochodzi. W przypadku zwracanego obiektu zawsze pochodzi on z wywołanej funkcji. Śledzenie stosu może zawierać wszystkie informacje potrzebne deweloperowi, ale może wystarczyłoby coś mniej ciężkiego i bardziej szczegółowego.

Don Kichot
źródło
-4

Odpowiedź brzmi tak. Zobacz http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions i otwórz strzałkę w przewodniku po stylu C ++ Google dotyczącym wyjątków. Widać tam argumenty, przed którymi się zdecydowali, ale powiedz, że gdyby musieli to zrobić od nowa, prawdopodobnie by zdecydowali.

Warto jednak zauważyć, że język Go również nie używa idiomatycznie wyjątków z podobnych powodów.

btilly
źródło
1
Przepraszam, ale nie rozumiem, w jaki sposób te wytyczne pomagają odpowiedzieć na pytanie: po prostu zalecają całkowite unikanie obsługi wyjątków. Nie o to pytam; moje pytanie brzmi, czy uzasadnione jest użycie typu wyjątku do opisania warunku błędu w sytuacji, gdy wynikowy obiekt wyjątku nigdy nie zostanie zgłoszony. Wydaje się, że w tych wytycznych nic nie ma na ten temat.
stakx