Wyjątki, kody błędów i dyskryminowane związki

80

Niedawno rozpocząłem programowanie w języku C #, ale mam spore doświadczenie w Haskell.

Ale rozumiem, że C # jest językiem zorientowanym obiektowo, nie chcę wciskać okrągłego kołka w kwadratowy otwór.

Przeczytałem artykuł Microsoft dotyczący wyjątku Zgłaszanie wyjątków :

NIE zwracaj kodów błędów.

Ale przyzwyczajony do Haskell, używałem typu danych C # OneOf, zwracając wynik jako „właściwą” wartość lub błąd (najczęściej wyliczenie) jako „lewą” wartość.

To bardzo przypomina konwencję Eitherw Haskell.

Wydaje mi się to bezpieczniejsze niż wyjątki. W języku C # ignorowanie wyjątków nie powoduje błędu kompilacji, a jeśli nie zostaną złapane, po prostu pękają i powodują awarię programu. Być może jest to lepsze niż ignorowanie kodu błędu i nieokreślone zachowanie, ale zawieszanie oprogramowania klienta nadal nie jest dobrą rzeczą, szczególnie gdy wykonuje wiele innych ważnych zadań biznesowych w tle.

Za pomocą OneOfnależy wyraźnie powiedzieć o rozpakowaniu i obsłudze wartości zwracanej i kodów błędów. A jeśli nie wiadomo, jak sobie z tym poradzić na tym etapie na stosie wywołań, należy wprowadzić wartość zwracaną przez bieżącą funkcję, aby wywołujący wiedzieli, że może to spowodować błąd.

Ale to nie wydaje się podejście sugerowane przez Microsoft.

Czy stosowanie OneOfzamiast wyjątków obsługi „zwykłych” wyjątków (takich jak nie znaleziono pliku itp.) Jest rozsądnym podejściem, czy też jest to okropna praktyka?


Warto zauważyć, że słyszałem, że wyjątki jako przepływ sterowania są uważane za poważny antypattern , więc jeśli „wyjątek” jest czymś, z czym normalnie byś sobie poradził bez kończenia programu, czyż nie jest to „przepływ sterowania”? Rozumiem, że jest tu trochę szarej strefy.

Uwaga: Nie używam OneOfdo takich rzeczy jak „Brak pamięci”, warunki, od których nie oczekuję powrotu do normy, nadal będą zgłaszać wyjątki. Ale wydaje mi się, że są to dość rozsądne problemy, takie jak dane wejściowe użytkownika, które nie analizują, są w zasadzie „kontrolą przepływu” i prawdopodobnie nie powinny generować wyjątków.


Kolejne przemyślenia:

Z tej dyskusji obecnie zabieram następujące rzeczy:

  1. Jeśli oczekujesz, że bezpośredni dzwoniący catchzajmie się wyjątkiem i będzie go obsługiwał przez większość czasu i będzie kontynuował pracę, być może inną ścieżką, prawdopodobnie powinien być częścią typu zwracanego. Optionallub OneOfmoże być przydatny tutaj.
  2. Jeśli oczekujesz, że bezpośredni dzwoniący nie będzie wychwytywał wyjątku przez większość czasu, wyrzuć wyjątek, aby zaoszczędzić głupoty ręcznego przekazywania go na stos.
  3. Jeśli nie masz pewności, co zrobi bezpośredni dzwoniący, może podać zarówno, jak Parsei TryParse.
Clinton
źródło
13
Użyj F # Result;-)
Astrinus
8
Trochę nie na temat, ale zauważ, że podejście, na które patrzysz, ma ogólną lukę, że nie ma sposobu, aby zapewnić, że deweloper poprawnie poradził sobie z błędem. Jasne, system pisania może działać jako przypomnienie, ale tracisz domyślną funkcję wysadzania programu w błąd w obsłudze, co zwiększa prawdopodobieństwo, że skończysz w nieoczekiwanym stanie. Jednak to, czy warto przypomnieć o utracie tego niewypłacalności, jest otwartą debatą. Skłaniam się do myślenia, że ​​lepiej jest napisać cały kod, zakładając, że wyjątek go kiedyś zakończy; wówczas przypomnienie ma mniejszą wartość.
jpmc26
9
Powiązany artykuł: Eric Lippert o czterech „wiadrach” wyjątków . Przykład, który podaje na temat egzogenicznych ekpuzji, pokazuje, że istnieją scenariusze, w których po prostu trzeba poradzić sobie z wyjątkami, tj FileNotFoundException.
Søren D. Ptæus
7
Mogę być w tym sam, ale myślę, że zawieszanie się jest dobre . Nie ma lepszych dowodów na istnienie problemu w oprogramowaniu niż dziennik awarii z prawidłowym śledzeniem. Jeśli masz zespół, który jest w stanie poprawić i wdrożyć łatkę w ciągu 30 minut lub krócej, pozwolenie na pojawienie się tych brzydkich kumplów może nie być złą rzeczą.
T. Sar
9
Jak to żadna z odpowiedzi wyjaśniono, że monada nie jest kod błędu , a nie jest ? Różnią się one zasadniczo, a zatem wydaje się, że pytanie opiera się na nieporozumieniu. (Chociaż w zmodyfikowanej formie jest to wciąż ważne pytanie.)EitherOneOf
Konrad Rudolph

Odpowiedzi:

131

ale zawieszanie oprogramowania klienta nadal nie jest dobrą rzeczą

Z pewnością jest to dobra rzecz.

Chcesz, aby wszystko, co pozostawia system w nieokreślonym stanie, zatrzymało system, ponieważ niezdefiniowany system może robić okropne rzeczy, takie jak uszkodzone dane, formatować dysk twardy i wysyłać wiadomości e-mail do prezydenta. Jeśli nie możesz odzyskać systemu i przywrócić go do określonego stanu, przyczyną awarii jest awaria. Właśnie dlatego budujemy systemy, które powodują awarie, a nie cicho się rozrywają. Teraz pewnie wszyscy chcemy stabilnego systemu, który nigdy się nie zawiesza, ale naprawdę chcemy tego tylko wtedy, gdy system pozostaje w zdefiniowanym przewidywalnym bezpiecznym stanie.

Słyszałem, że wyjątki w zakresie kontroli są uważane za poważny antypattern

To absolutnie prawda, ale często jest niezrozumiana. Kiedy wymyślili system wyjątków, bali się, że łamią programowanie strukturalne. Programowanie strukturalne dlatego mamy for, while, until, break, i continuegdy wszystko, czego potrzebujemy, aby zrobić wszystko, że jest goto.

Dijkstra nauczył nas, że używanie goto w sposób nieformalny (czyli skakanie w dowolne miejsce) sprawia, że ​​czytanie kodu jest koszmarem. Kiedy dali nam system wyjątków, bali się, że wymyślają na nowo goto. Dlatego powiedzieli nam, aby nie „używać go do kontroli przepływu”, mając nadzieję, że zrozumiemy. Niestety wielu z nas tego nie zrobiło.

O dziwo, często nie nadużywamy wyjątków do tworzenia kodu spaghetti, jak to było w przypadku goto. Wydaje się, że sama rada spowodowała więcej problemów.

Zasadniczo wyjątki dotyczą odrzucenia założenia. Gdy poprosisz o zapisanie pliku, zakładasz, że plik może i zostanie zapisany. Wyjątek występuje, gdy nie może być to niezgodne z prawem, że HD jest pełny lub ponieważ szczur wgryzł się w kabel danych. Możesz obsłużyć wszystkie te błędy w różny sposób, możesz obsłużyć je wszystkie w ten sam sposób lub możesz pozwolić im zatrzymać system. W twoim kodzie jest szczęśliwa ścieżka, w której twoje założenia muszą być prawdziwe. Tak czy inaczej wyjątki zabierają cię z tej szczęśliwej ścieżki. Ściśle mówiąc, tak, to rodzaj „kontroli przepływu”, ale nie o to ci ostrzegali. Mówili o takich bzdurach :

wprowadź opis zdjęcia tutaj

„Wyjątki powinny być wyjątkowe”. Ta mała tautologia narodziła się, ponieważ projektanci systemów wyjątków potrzebują czasu na tworzenie śladów stosu. W porównaniu do skakania jest to powolne. Zjada czas procesora. Ale jeśli masz zamiar zalogować się i zatrzymać system lub przynajmniej zatrzymać bieżące intensywne przetwarzanie przed uruchomieniem następnego, masz trochę czasu na zabicie. Jeśli ludzie zaczną używać wyjątków „do kontroli przepływu”, te założenia dotyczące czasu znikną z okna. Tak więc „wyjątki powinny być wyjątkowe” zostały nam przekazane jako wzgląd na wydajność.

Znacznie ważniejsze niż to nas nie myli. Jak długo zajęło ci zauważenie nieskończonej pętli w powyższym kodzie?

NIE zwracaj kodów błędów.

... to dobra rada, gdy jesteś w bazie kodu, która zwykle nie używa kodów błędów. Dlaczego? Ponieważ nikt nie będzie pamiętać, aby zapisać wartość zwracaną i sprawdzić kody błędów. To wciąż dobra konwencja, gdy jesteś w C.

OneOf

Używasz innej konwencji. W porządku, o ile ustanawiasz konwencję, a nie tylko walczysz z inną. Mylące są dwie konwencje błędów w tej samej bazie kodu. Jeśli w jakiś sposób pozbyłeś się całego kodu korzystającego z innej konwencji, to idź dalej.

Sam lubię konwencję. Jedno z najlepszych wyjaśnień, jakie tu znalazłem * :

wprowadź opis zdjęcia tutaj

Ale bardzo mi się podoba i nadal nie będę mieszał go z innymi konwencjami. Wybierz jeden i trzymaj się go. 1

1: Rozumiem przez to, że nie myślę o więcej niż jednej konwencji w tym samym czasie.


Kolejne przemyślenia:

Z tej dyskusji obecnie zabieram następujące rzeczy:

  1. Jeśli oczekujesz, że bezpośredni rozmówca będzie przechwytywał wyjątek przez większość czasu i kontynuował pracę, być może inną ścieżką, prawdopodobnie powinien być częścią typu zwracanego. Opcjonalne lub OneOf mogą być przydatne tutaj.
  2. Jeśli oczekujesz, że bezpośredni dzwoniący nie będzie wychwytywał wyjątku przez większość czasu, wyrzuć wyjątek, aby zaoszczędzić głupoty ręcznego przekazywania go na stos.
  3. Jeśli nie masz pewności, co zrobi bezpośredni rozmówca, być może podaj oba, takie jak Parse i TryParse.

To naprawdę nie jest takie proste. Jedną z podstawowych rzeczy, które musisz zrozumieć, jest zero.

Ile dni pozostało w maju? 0 (ponieważ to nie jest maj. To już czerwiec).

Wyjątki są sposobem na odrzucenie założenia, ale nie są jedynym sposobem. Jeśli użyjesz wyjątków, aby odrzucić założenie, opuścisz szczęśliwą ścieżkę. Ale jeśli wybrałeś wartości, aby zesłać szczęśliwą ścieżkę, która sygnalizuje, że rzeczy nie są tak proste, jak zakładano, możesz pozostać na tej ścieżce, dopóki będzie ona w stanie poradzić sobie z tymi wartościami. Czasami 0 jest już używane, aby coś oznaczać, więc musisz znaleźć inną wartość, na którą odwzorujesz swój pomysł odrzucenia założenia. Możesz rozpoznać ten pomysł po jego użyciu w starej dobrej algebrze . Monady mogą w tym pomóc, ale nie zawsze musi to być monada.

Na przykład 2 :

IList<int> ParseAllTheInts(String s) { ... }

Czy potrafisz wymyślić jakiś dobry powód, dla którego musi to być zaprojektowane tak, aby celowo rzucało cokolwiek? Zgadnij, co otrzymasz, gdy nie można przeanalizować żadnego elementu int? Nawet nie muszę ci mówić.

To znak dobrego imienia. Przepraszam, ale TryParse nie jest moim pomysłem na dobre imię.

Często unikamy rzucania wyjątku na nieotrzymanie niczego, gdy odpowiedź może być więcej niż jedną rzeczą w tym samym czasie, ale z jakiegoś powodu, jeśli odpowiedź jest albo jedna, albo niczym, mamy obsesję na punkcie nalegania, aby dać nam jedną rzecz lub rzucić:

IList<Point> Intersection(Line a, Line b) { ... }

Czy linie równoległe naprawdę muszą powodować tutaj wyjątek? Czy to naprawdę takie złe, jeśli ta lista nigdy nie będzie zawierać więcej niż jednego punktu?

Może semantycznie po prostu nie możesz tego znieść. Jeśli tak, szkoda. Ale może Monady, które nie mają tak dowolnych rozmiarów, jak Listto, sprawią, że poczujesz się lepiej.

Maybe<Point> Intersection(Line a, Line b) { ... }

Monady to małe kolekcje specjalnego przeznaczenia, które mają być używane w określony sposób, dzięki czemu nie trzeba ich testować. Powinniśmy znaleźć sposoby radzenia sobie z nimi bez względu na to, co zawierają. W ten sposób szczęśliwa ścieżka pozostaje prosta. Jeśli otworzysz się i przetestujesz każdą dotkniętą Monadę, używasz ich źle.

Wiem, to dziwne. Ale to nowe narzędzie (cóż, dla nas). Daj więc trochę czasu. Młotki mają większy sens, gdy przestajesz używać ich na śrubach.


Jeśli mi pozwolisz, chciałbym odpowiedzieć na ten komentarz:

Dlaczego żadna z odpowiedzi nie wyjaśnia, że ​​Albo monada nie jest kodem błędu, ani OneOf? Różnią się one zasadniczo, a zatem wydaje się, że pytanie opiera się na nieporozumieniu. (Chociaż w zmodyfikowanej formie jest to wciąż ważne pytanie.) - Konrad Rudolph 4 czerwca 18 o 14:08

To jest absolutna prawda. Monady są znacznie bliżej kolekcji niż wyjątki, flagi lub kody błędów. Robią dobre pojemniki na takie rzeczy, jeśli są mądrze używane.

candied_orange
źródło
9
Czy nie byłoby właściwsze, gdyby czerwony ślad znajdował się pod polami, a przez to nie przebiegał przez nie?
Flater
4
Logicznie masz rację. Jednak każde pole na tym konkretnym diagramie reprezentuje operację wiązania monadycznego. bind zawiera (być może tworzy!) czerwony ślad. Polecam obejrzeć pełną prezentację. To interesujące, a Scott jest świetnym mówcą.
Gusdor
7
Myślę o wyjątkach bardziej jak o instrukcji COMEFROM niż GOTO, ponieważ throwtak naprawdę nie wiem / nie mówimy, dokąd skaczemy;)
Warbo
3
Nie ma nic złego w mieszaniu konwencji, jeśli można ustalić granice, na tym właśnie polega enkapsulacja.
Robert Harvey
4
Cała pierwsza część tej odpowiedzi brzmi mocno, jakbyś przestał czytać pytanie po cytowanym zdaniu. Pytanie potwierdza, że ​​zawieszenie programu jest lepsze niż przejście do niezdefiniowanego zachowania: kontrastują one z awariami w czasie wykonywania nie z ciągłym, niezdefiniowanym wykonaniem, ale raczej z błędami w czasie kompilacji, które uniemożliwiają nawet zbudowanie programu bez uwzględnienia potencjalnego błędu i radzenie sobie z tym (co może zakończyć się awarią, jeśli nie będzie nic więcej do zrobienia, ale będzie to awaria, ponieważ chcesz, aby tak się stało, a nie dlatego, że zapomniałeś).
KRyan
35

C # to nie Haskell i powinieneś postępować zgodnie z konsensusem ekspertów społeczności C #. Jeśli zamiast tego spróbujesz zastosować praktyki Haskell w projekcie C #, zrazisz wszystkich w zespole, a ostatecznie prawdopodobnie odkryjesz powody, dla których społeczność C # robi różne rzeczy. Jednym z głównych powodów jest to, że C # nie obsługuje wygodnie dyskryminowanych związków.

Słyszałem, że wyjątki związane z kontrolą są uważane za poważny antypattern,

To nie jest powszechnie akceptowana prawda. Wybór w każdym języku, który obsługuje wyjątki, to albo rzucić wyjątek (którego osoba dzwoniąca może nie obsłużyć), albo zwrócić pewną wartość złożoną (którą MUSI obsługiwać osoba dzwoniąca).

Propagowanie warunków błędu w górę przez stos wywołań wymaga warunku na każdym poziomie, podwajając cykliczność złożoności tych metod, aw konsekwencji podwajając liczbę przypadków testowych w jednostce. W typowych aplikacjach biznesowych wiele wyjątków nie można odzyskać i można je propagować na najwyższym poziomie (np. Punkt wejścia usługi aplikacji sieci web).

Kevin Cline
źródło
9
Rozmnażanie monad nie wymaga niczego.
Basilevs,
23
@Basilevs Wymaga obsługi propagacji monad przy jednoczesnym zachowaniu czytelności kodu, czego nie ma C #. Dostajesz powtarzające się instrukcje if-return lub łańcuchy lambdów.
Sebastian Redl,
4
@SebastianRedl lambdas wyglądają OK w C #.
Basilevs
@Basilevs Z punktu widzenia składni tak, ale podejrzewam, że z punktu widzenia wydajności mogą być mniej atrakcyjne.
Pharap
5
We współczesnym języku C # prawdopodobnie można częściowo użyć funkcji lokalnych, aby odzyskać trochę prędkości. W każdym razie większość oprogramowania biznesowego jest znacznie wolniej spowalniana przez IO niż inne.
Turksarama,
26

Przybyłeś do C # w interesującym czasie. Do niedawna język był mocno osadzony w imperatywnej przestrzeni programowania. Stosowanie wyjątków do komunikowania błędów było zdecydowanie normą. Używanie kodów zwrotnych cierpiało z powodu braku wsparcia językowego (np. Wokół dyskryminowanych związków i dopasowywania wzorców). Całkiem rozsądnie oficjalne wytyczne firmy Microsoft miały na celu unikanie kodów powrotu i stosowanie wyjątków.

Zawsze jednak istniały wyjątki w postaci TryXXXmetod, które booleanzwracałyby wynik powodzenia i dostarczały wynik drugiej wartości poprzez outparametr. Są one bardzo podobne do wzorców „wypróbowania” w programowaniu funkcjonalnym, z tym wyjątkiem, że wynik pochodzi z parametru wyjściowego, a nie z Maybe<T>wartości zwracanej.

Ale rzeczy się zmieniają. Programowanie funkcjonalne staje się jeszcze bardziej popularne, a języki takie jak C # reagują. C # 7.0 wprowadził pewne podstawowe funkcje dopasowania wzorców do języka; C # 8 wprowadzi znacznie więcej, w tym wyrażenie przełączające, wzorce rekurencyjne itp. Wraz z tym nastąpił rozwój „bibliotek funkcjonalnych” dla C #, takich jak moja biblioteka Succinc <T> , która zapewnia również wsparcie dla dyskryminowanych związków .

Te dwie rzeczy razem oznaczają, że kod podobny do następującego powoli zyskuje na popularności.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

W tej chwili jest wciąż dość niszowa, chociaż nawet w ciągu ostatnich dwóch lat zauważyłem wyraźną zmianę z ogólnej wrogości wśród programistów C # na taki kod na rosnące zainteresowanie wykorzystaniem takich technik.

Nie mówimy jeszcze „ NIE zwracaj kodów błędów”. jest przestarzały i wymaga przejścia na emeryturę. Ale marsz drogą do tego celu jest już w toku. Wybierz więc: trzymaj się starych sposobów zgłaszania wyjątków przez gejowską rezygnację, jeśli tak lubisz robić rzeczy; lub zacznij odkrywać nowy świat, w którym konwencje z języków funkcjonalnych stają się coraz bardziej popularne w C #.

David Arno
źródło
22
Dlatego nienawidzę C # :) Ciągle dodają sposoby na skórowanie kota i oczywiście stare sposoby nigdy nie znikają. W końcu nie ma żadnych konwencji, perfekcjonista może godzinami decydować, która metoda jest „ładniejsza”, a nowy programista musi się wszystkiego nauczyć, zanim będzie w stanie odczytać kod innych.
Aleksandr Dubinsky
24
@AleksandrDubinsky, natrafiłeś na podstawowy problem z językami programowania w ogóle, nie tylko C #. Tworzone są nowe języki, szczupłe, świeże i pełne nowoczesnych pomysłów. I bardzo niewielu ludzi ich używa. Z czasem zyskują użytkowników i nowe funkcje, przykręcone do starych funkcji, których nie można usunąć, ponieważ spowodowałoby to uszkodzenie istniejącego kodu. Wzdęcia rosną. Tak więc ludzie tworzą nowe języki, które są szczupłe, świeże i pełne nowoczesnych pomysłów ...
David Arno
7
@AleksandrDubinsky nie nienawidzisz, gdy dodają teraz funkcje z lat 60.
ctrl-alt-delor
6
@AleksandrDubinsky Tak. Ponieważ Java próbowała dodać coś raz (ogólne), to się nie udało, teraz musimy żyć z okropnym bałaganem, który się
pojawił,
3
@Agent_L: Wiesz, że Java została dodana java.util.Stream(podobnie jak IE półliczna ) i lambda, prawda? :)
cHao
5

Wydaje mi się, że użycie DU do zwrócenia błędów jest całkowicie uzasadnione, ale powiedziałbym to tak, jak napisałem OneOf :) Ale powiedziałbym to tak, ponieważ jest to powszechna praktyka w F # itp. (Np. Typ wyniku, jak wspomniali inni ).

Nie myślę o błędach, które zwykle powracam, jako wyjątkowych sytuacjach, ale raczej jako normalnych, ale mam nadzieję, że rzadko spotykane sytuacje, które mogą lub nie muszą być obsługiwane, ale powinny być modelowane.

Oto przykład ze strony projektu.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Zgłaszanie wyjątków dla tych błędów wydaje się przesadą - mają one narzut związany z wydajnością, oprócz wad polegających na tym, że nie są wyraźne w sygnaturze metody lub zapewniają wyczerpujące dopasowanie.

W jakim stopniu OneOf jest idiomatyczny w c #, to kolejne pytanie. Składnia jest poprawna i względnie intuicyjna. DU i programowanie zorientowane na szyny są dobrze znanymi koncepcjami.

Zapisałbym wyjątki dla rzeczy, które wskazują, że coś jest zepsute tam, gdzie to możliwe.

mcintyre321
źródło
Mówisz, że OneOfchodzi o to, aby wartość zwracana była bogatsza, jak zwracanie wartości Nullable. Zastępuje wyjątki w sytuacjach, które na początku nie są wyjątkowe.
Aleksandr Dubinsky
Tak - i zapewnia łatwy sposób wykonania pełnego dopasowania w abonencie wywołującym, co nie jest możliwe przy użyciu hierarchii wyników opartej na dziedziczeniu.
mcintyre321
4

OneOfjest mniej więcej równoważny sprawdzonym wyjątkom w Javie. Istnieją pewne różnice:

  • OneOfnie działa z metodami wywołującymi metody wywołujące ich skutki uboczne (ponieważ można je sensownie wywołać, a wynik po prostu zignorować). Oczywiście wszyscy powinniśmy starać się używać czystych funkcji, ale nie zawsze jest to możliwe.

  • OneOfnie zawiera informacji o tym, gdzie wystąpił problem. Jest to dobre, ponieważ pozwala zaoszczędzić wydajność (zgłoszone wyjątki w Javie są kosztowne ze względu na wypełnienie śladu stosu, aw języku C # będzie to samo) i zmusza do podania wystarczających informacji w elemencie błędu OneOf. Jest to również złe, ponieważ informacje te mogą być niewystarczające i trudno jest znaleźć źródło problemu.

OneOf i zaznaczony wyjątek ma jedną wspólną „wspólną cechę”:

  • oba zmuszają cię do obsługi ich wszędzie w stosie wywołań

To zapobiega twojemu strachowi

W języku C # ignorowanie wyjątków nie powoduje błędu kompilacji, a jeśli nie zostaną złapane, po prostu pękają i powodują awarię programu.

ale jak już powiedziano, strach ten jest irracjonalny. Zasadniczo w twoim programie jest zwykle jedno miejsce, w którym musisz wszystko złapać (istnieje szansa, że ​​użyjesz już frameworka). Ponieważ Ty i Twój zespół spędzacie zwykle tygodnie lub lata pracując nad programem, nie zapomnicie go, prawda?

Dużą zaletą ignorowanych wyjątków jest to, że można je obsługiwać wszędzie tam, gdzie chcesz je obsłużyć, a nie wszędzie w śladzie stosu. To eliminuje masę bojlerów (wystarczy spojrzeć na niektóre kody Java deklarujące „rzuty…” lub owijanie wyjątków), a także sprawia, że ​​kod jest mniej problematyczny: Jakakolwiek metoda może rzucić, musisz być tego świadomy. Na szczęście właściwa akcja zwykle nic nie robi , tzn. Pozwala bąbelkowi dotrzeć do miejsca, w którym można rozsądnie sobie z tym poradzić.

maaartinus
źródło
+1, ale dlaczego OneOf nie działa z efektami ubocznymi? (Myślę, że dzieje się tak, ponieważ przejdzie kontrolę, jeśli nie przypiszesz wartości zwracanej, ale ponieważ nie znam OneOf ani dużego C # nie jestem pewien - wyjaśnienie poprawiłoby twoją odpowiedź.)
dcorking
1
Możesz zwrócić informacje o błędzie za pomocą OneOf, sprawiając, że zwrócony obiekt błędu zawiera przydatne informacje, np. OneOf<SomeSuccess, SomeErrorInfo>Gdzie SomeErrorInfozawiera szczegółowe informacje o błędzie.
mcintyre321
@dcorking Edytowane. Jasne, jeśli zignorujesz wartość zwracaną, to nic nie wiesz o tym, co się stało. Jeśli robisz to z czystą funkcją, jest w porządku (po prostu zmarnowałeś czas).
maaartinus
2
@ mcintyre321 Jasne, możesz dodać informacje, ale musisz to zrobić ręcznie, a to może być lenistwo i zapomnieć. Myślę, że w bardziej skomplikowanych przypadkach śledzenie stosu jest bardziej pomocne.
maaartinus
4

Czy używanie OneOf zamiast wyjątków do obsługi „zwykłych” wyjątków (takich jak nie znaleziono pliku itp.) Jest rozsądnym podejściem, czy też jest to okropna praktyka?

Warto zauważyć, że słyszałem, że wyjątki jako przepływ sterowania są uważane za poważny antypattern, więc jeśli „wyjątek” jest czymś, z czym normalnie byś sobie poradził bez kończenia programu, czyż nie jest to „przepływ sterowania”?

Błędy mają wiele wymiarów. Identyfikuję trzy wymiary: czy można im zapobiec, jak często występują i czy można je odzyskać. Tymczasem sygnalizacja błędów ma głównie jeden wymiar: decydowanie, w jakim stopniu pokonać dzwoniącego nad głowę, aby zmusić go do obsługi błędu, lub pozwolić, aby błąd „cicho” wybuchł jako wyjątek.

Błędy, których nie można zapobiec, częste i możliwe do odzyskania błędy, to te, które naprawdę muszą zostać naprawione w miejscu połączenia. Najlepiej uzasadniają zmuszenie dzwoniącego do skonfrontowania się z nimi. W Javie sprawiam, że sprawdzają wyjątki, a podobny efekt uzyskuje się, stosując Try*metody lub zwracając dyskryminację unii. Zróżnicowane związki są szczególnie przydatne, gdy funkcja ma wartość zwracaną. Są znacznie bardziej dopracowaną wersją powrotu null. Składnia try/catchbloków nie jest świetna (zbyt wiele nawiasów), dzięki czemu alternatywy wyglądają lepiej. Ponadto wyjątki są nieco powolne, ponieważ rejestrują ślady stosu.

Błędy, których można uniknąć (których nie można uniknąć z powodu błędu / zaniedbania programisty) i błędy niemożliwe do odzyskania działają naprawdę dobrze jako zwykłe wyjątki. Rzadkie błędy działają również lepiej niż zwykłe wyjątki, ponieważ programista często może ważyć, że nie warto tego przewidywać (zależy to od celu programu).

Ważną kwestią jest to, że często witryna użytkowania określa, w jaki sposób tryby błędów metody pasują do tych wymiarów, o czym należy pamiętać, rozważając następujące kwestie.

Z mojego doświadczenia wynika, że ​​zmuszanie dzwoniącego do stawienia czoła warunkom błędu jest w porządku, gdy błędom nie można zapobiec, często i można je odzyskać, ale staje się bardzo denerwujące, gdy dzwoniący jest zmuszony przeskakiwać przez obręcze, gdy nie potrzebuje lub nie chce . Może to być spowodowane tym, że osoba dzwoniąca wie, że błąd się nie zdarzy, lub dlatego, że (jeszcze) nie chce, aby kod był odporny. W Javie wyjaśnia to niepokój związany z częstym używaniem sprawdzonych wyjątków i zwracaniem Opcjonalnego (który jest podobny do Być może i został niedawno wprowadzony z powodu wielu kontrowersji). W razie wątpliwości zgłaszaj wyjątki i pozwól dzwoniącemu zdecydować, w jaki sposób chcą je obsłużyć.

Na koniec należy pamiętać, że najważniejszą rzeczą w warunkach błędu, znacznie przewyższającą wszelkie inne czynniki, takie jak sposób ich sygnalizowania, jest dokładne ich udokumentowanie .

Aleksandr Dubinsky
źródło
2

W innych odpowiedziach omówiono wyjątki kontra kody błędów wystarczająco szczegółowo, dlatego chciałbym dodać kolejną perspektywę specyficzną dla pytania:

OneOf nie jest kodem błędu, jest raczej monadą

Sposób, w jaki jest używany, OneOf<Value, Error1, Error2>jest kontenerem, który reprezentuje rzeczywisty wynik lub stan błędu. To jest tak Optional, z wyjątkiem tego, że gdy nie ma żadnej wartości, może podać więcej szczegółów, dlaczego tak jest.

Głównym problemem z kodami błędów jest to, że zapominasz je sprawdzić. Ale tutaj dosłownie nie można uzyskać dostępu do wyniku bez sprawdzania błędów.

Jedynym problemem jest to, że nie dbasz o wynik. Przykład z dokumentacji OneOf podaje:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... a następnie tworzą różne odpowiedzi HTTP dla każdego wyniku, co jest znacznie lepsze niż stosowanie wyjątków. Ale jeśli wywołasz taką metodę.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

wtedy nie ma problemu, a wyjątki byłoby lepszym wyborem. Porównaj Rail saveVS save!.

Głowonóg
źródło
1

Jedną rzeczą, którą należy wziąć pod uwagę, jest to, że kod w języku C # zasadniczo nie jest czysty. W bazie kodu pełnej efektów ubocznych i przy kompilatorze, który nie dba o to, co robisz ze zwracaną wartością jakiejś funkcji, twoje podejście może wywołać znaczny smutek. Na przykład, jeśli masz metodę, która usuwa plik, chcesz się upewnić, że aplikacja powiadomi, gdy metoda się nie powiedzie, nawet jeśli nie ma powodu, aby sprawdzać kod błędu „na szczęśliwej ścieżce”. Jest to znacząca różnica w stosunku do języków funkcjonalnych, które wymagają jawnego zignorowania wartości zwrotnych, których nie używasz. W języku C # zwracane wartości są zawsze domyślnie ignorowane (jedynym wyjątkiem są moduły pobierające właściwości IIRC).

Powodem, dla którego MSDN mówi „nie zwracać kodów błędów” jest właśnie to - nikt nie zmusza cię nawet do odczytania kodu błędu. Kompilator wcale ci nie pomaga. Jednakże, jeśli czynność jest efektem ubocznym wolne, to można bezpiecznie używać coś takiego Either- chodzi o to, że nawet jeśli zignorować wynik błędu (jeśli w ogóle), można tylko zrobić, jeśli nie korzystać z „właściwego rezultatu” zarówno. Jest kilka sposobów, jak to osiągnąć - na przykład możesz zezwolić na „odczytanie” wartości, przekazując delegata do obsługi przypadków sukcesu i błędów, i możesz łatwo połączyć to z podejściami takimi jak programowanie zorientowane na kolej ( działa dobrze w C #).

int.TryParsejest gdzieś pośrodku. To jest czyste. Definiuje to, jakie są wartości dwóch wyników przez cały czas - więc wiesz, że jeśli zwracana jest wartość false, parametr wyjściowy zostanie ustawiony na 0. Nadal nie przeszkadza ci to w użyciu parametru wyjściowego bez sprawdzenia wartości zwracanej, ale przynajmniej masz gwarancję tego, jaki będzie wynik, nawet jeśli funkcja się nie powiedzie.

Ale jedną absolutnie kluczową rzeczą jest tutaj konsekwencja . Kompilator nie uratuje cię, jeśli ktoś zmieni tę funkcję, aby mieć skutki uboczne. Lub rzucać wyjątki. Więc chociaż takie podejście jest w porządku w C # (i ja go używam), musisz upewnić się, że wszyscy w Twoim zespole go rozumieją i używają . Wiele niezmienników wymaganych do sprawnego programowania funkcjonalnego nie jest egzekwowanych przez kompilator C # i musisz upewnić się, że są one śledzone przez ciebie. Musisz zdawać sobie sprawę z tego, co się psuje, jeśli nie postępujesz zgodnie z regułami, które Haskell domyślnie egzekwuje - i jest to coś, o czym pamiętasz w przypadku wszelkich funkcjonalnych paradygmatów, które wprowadzasz do swojego kodu.

Luaan
źródło
0

Poprawnym stwierdzeniem byłoby: Nie używaj kodów błędów jako wyjątków! I nie używaj wyjątków dla wyjątków.

Jeśli dzieje się coś, z czego Twój kod nie może się odzyskać (tzn. Sprawić, aby działał), jest to wyjątek. Nie ma nic, co twój bezpośredni kod zrobiłby z kodem błędu, poza przekazaniem go do góry, aby się zalogować lub być może w jakiś sposób przekazać kod użytkownikowi. Jednak właśnie po to są wyjątki - zajmują się całym przekazaniem i pomagają analizować, co się naprawdę wydarzyło.

Jeśli jednak zdarzy się coś, na przykład nieprawidłowe dane wejściowe, które można obsłużyć, albo automatycznie formatując je ponownie, albo prosząc użytkownika o poprawienie danych (i pomyślałeś o tym przypadku), to nie jest tak naprawdę wyjątkiem - tak jak ty spodziewać się tego. Możesz obsłużyć te przypadki z kodami błędów lub dowolną inną architekturą. Po prostu nie wyjątki.

(Zauważ, że nie mam dużego doświadczenia w języku C #, ale moja odpowiedź powinna dotyczyć ogólnego przypadku opartego na poziomie koncepcyjnym wyjątków).

Frank Hopkins
źródło
7
Zasadniczo nie zgadzam się z przesłanką tej odpowiedzi. Jeśli projektanci języków nie zamierzają stosować wyjątków w przypadku błędów, które można naprawić, catchsłowo kluczowe nie istnieje. A ponieważ rozmawiamy w kontekście C #, warto zauważyć, że w filozofii, której się tu przyświeca, nie stosuje się środowiska .NET; być może najprostszym możliwym do wyobrażenia przykładem „nieprawidłowych danych wejściowych, które można obsłużyć”, jest wpisanie przez użytkownika wartości innej niż liczba w polu, w którym spodziewana jest liczba… i int.Parsezgłasza wyjątek.
Mark Amery
@MarkAmery Dla int.Parse nie jest to nic, z czego mógłby odzyskać, ani nic, czego by się spodziewał. Klauzula catch jest zwykle używana, aby uniknąć całkowitej awarii z widoku zewnętrznego, nie poprosi użytkownika o nowe poświadczenia bazy danych itp., Uniknie to awarii programu. To awaria techniczna, a nie kontrola kontroli aplikacji.
Frank Hopkins
3
Kwestia, czy lepiej, aby funkcja zgłosiła wyjątek, gdy wystąpi warunek, zależy od tego, czy bezpośredni wywoływacz funkcji może z łatwością poradzić sobie z tym warunkiem. W przypadkach, w których jest to możliwe, zgłoszenie wyjątku, który musi przechwycić bezpośredni rozmówca, stanowi dodatkową pracę dla autora kodu wywołującego i dodatkową pracę dla maszyny przetwarzającej go. Jeśli jednak osoba dzwoniąca nie będzie przygotowana do obsługi warunku, wyjątki zmniejszają potrzebę jawnego kodowania go.
supercat
@Darkwing „Możesz obsłużyć te przypadki z kodami błędów lub dowolną inną architekturą”. Tak, możesz to zrobić. Jednak .NET nie otrzymuje za to żadnej pomocy, ponieważ sam .NET CL używa wyjątków zamiast kodów błędów. Więc nie tylko w wielu przypadkach jesteś zmuszony wychwycić wyjątki .NET i zwrócić odpowiedni kod błędu zamiast pozwolić, aby wyjątek wybuchł, ale również zmuszasz innych członków swojego zespołu do innej koncepcji obsługi błędów, z której muszą korzystać równolegle z wyjątkami, standardowa koncepcja obsługi błędów.
Aleksander
-2

To „Straszny pomysł”.

Jest to okropne, ponieważ przynajmniej w .net nie ma wyczerpującej listy możliwych wyjątków. Każdy kod może generować dowolny wyjątek. Zwłaszcza jeśli robisz OOP i możesz wywoływać przesłonięte metody na podtypie, a nie na typie zadeklarowanym

Musisz więc zmienić wszystkie metody i funkcje na OneOf<Exception, ReturnType>, a następnie, jeśli chcesz obsługiwać różne typy wyjątków, musisz sprawdzić typ If(exception is FileNotFoundException)itp.

try catch jest odpowiednikiem OneOf<Exception, ReturnType>

Edytować ----

Wiem, że proponujesz powrót, OneOf<ErrorEnum, ReturnType>ale wydaje mi się, że jest to różnica bez różnicy.

Łączymy kody powrotu, oczekiwane wyjątki i rozgałęzienia według wyjątków. Ale ogólny efekt jest taki sam.

Ewan
źródło
2
„Normalne” wyjątki to także okropny pomysł. wskazówka jest w nazwie
Ewan
4
Nie proponuję powrotu Exception.
Clinton
czy proponujesz, aby alternatywą dla jednego z nich był przepływ kontroli poprzez wyjątki
Ewan