Czy można używać wyjątków jako narzędzi do wczesnego „wychwytywania” błędów?

29

Używam wyjątków, aby wcześnie wychwytywać problemy. Na przykład:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Mój program nigdy nie powinien przejść nulltej funkcji. Nigdy tego nie zamierzam. Jednak, jak wszyscy wiemy, w programowaniu zdarzają się niezamierzone rzeczy.

Zgłoszenie wyjątku, jeśli wystąpi ten problem, pozwala mi go wykryć i naprawić, zanim spowoduje więcej problemów w innych miejscach programu. Wyjątek zatrzymuje program i mówi mi: „wydarzyły się tutaj złe rzeczy, napraw je”. Zamiast tego nullporuszać się po programie, powodując problemy w innym miejscu.

Masz rację, w tym przypadku po nullprostu spowodowałoby to NullPointerExceptionod razu, więc może nie być najlepszym przykładem.

Ale rozważ taką metodę jak na przykład:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

W takim przypadku nullparametr a byłby przekazywany dookoła programu i może powodować błędy znacznie później, co będzie trudne do odtworzenia.

Zmiana funkcji tak:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Pozwala mi dostrzec problem znacznie, zanim spowoduje dziwne błędy w innych miejscach.

Ponadto, nullodniesienia jako parametr jest jedynie przykładem. Może to być wiele problemów, od nieprawidłowych argumentów po cokolwiek innego. Zawsze lepiej jest je wcześnie wykryć.


Więc moje pytanie brzmi: czy to dobra praktyka? Czy moje wykorzystanie wyjątków jako narzędzi zapobiegających problemom jest dobre? Czy jest to uzasadnione stosowanie wyjątków, czy może jest problematyczne?

Aviv Cohn
źródło
2
Czy downvoter może wyjaśnić, co jest nie tak z tym pytaniem?
Giorgio
2
„katastrofa wcześnie, katastrofa często”
Bryan Chen
16
Właśnie z tego powodu istnieją wyjątki.
raptortech97
Pamiętaj, że umowy na kodowanie umożliwiają wykrycie wielu takich błędów w sposób statyczny. Jest to generalnie lepsze niż zgłaszanie wyjątku w czasie wykonywania. Wsparcie i skuteczność umów na kodowanie różni się w zależności od języka.
Brian
3
Ponieważ zgłaszasz IllegalArgumentException, niepotrzebne jest mówienie „ Input person”.
kevin cline

Odpowiedzi:

29

Tak, „wczesne niepowodzenie” to bardzo dobra zasada i jest to po prostu jeden z możliwych sposobów jej wdrożenia. A w metodach, które muszą zwracać określoną wartość, tak naprawdę niewiele można zrobić celowo, by zawieść - albo generowanie wyjątków, albo wyzwalanie asercji. Wyjątki mają sygnalizować „wyjątkowe” warunki, a wykrycie błędu programowania z pewnością jest wyjątkowe.

Kilian Foth
źródło
Wczesne niepowodzenie to dobry sposób, jeśli nie ma sposobu na odzyskanie po błędzie lub stanie. Na przykład, jeśli chcesz skopiować plik, a ścieżka źródłowa lub docelowa jest pusta, powinieneś natychmiast zgłosić wyjątek.
Michael Shopsin
Jest również znany jako „fail fast”
keuleJ
8

Tak, rzucanie wyjątków to dobry pomysł. Rzuć je wcześnie, rzuć je często, rzuć je chętnie.

Wiem, że jest debata na temat „wyjątków vs. asercji”, z pewnymi wyjątkowymi zachowaniami (szczególnie tymi, które mają odzwierciedlać błędy programistyczne) obsługiwanymi przez asercje, które można „skompilować” dla środowiska wykonawczego zamiast debugowania / testowania kompilacji. Ale wydajność zużywana podczas kilku dodatkowych kontroli poprawności jest minimalna na nowoczesnym sprzęcie, a wszelkie dodatkowe koszty są znacznie większe niż wartość prawidłowych, nieuszkodzonych wyników. Tak naprawdę nigdy nie spotkałem bazy kodu aplikacji, dla której chciałbym (większość) kontroli usuniętych w czasie wykonywania.

Kusi mnie, aby powiedzieć, że nie chciałbym dużo dodatkowych kontroli i warunków w ciasnych pętlach intensywnego numerycznie kodu ... ale tak naprawdę generuje się wiele błędów numerycznych, a jeśli nie zostanie tam złapany, rozprzestrzeni się na zewnątrz wpływ na wszystkie wyniki. Więc nawet tam warto sprawdzić. W rzeczywistości niektóre z najlepszych, najbardziej wydajnych algorytmów numerycznych opierają się na ocenie błędów.

Ostatnim miejscem, w którym należy być bardzo świadomym dodatkowego kodu, jest kod wrażliwy na opóźnienia, w którym dodatkowe warunki warunkowe mogą powodować utknięcie rurociągu. Tak więc w środku systemu operacyjnego, DBMS i innych jąder oprogramowania pośredniego oraz obsługi komunikacji / protokołu na niskim poziomie. Ale znowu, są to niektóre z miejsc, w których najczęściej można zaobserwować błędy, a ich skutki (bezpieczeństwo, poprawność i integralność danych) są najbardziej szkodliwe.

Jednym z ulepszeń, które znalazłem, jest nierzucanie tylko wyjątków na poziomie podstawowym. IllegalArgumentExceptionjest dobry, ale może pochodzić z dowolnego miejsca. W większości języków dodanie niestandardowych wyjątków nie zajmuje wiele. W przypadku modułu obsługi osób powiedz:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Wtedy, gdy ktoś zobaczy PersonArgumentException, jasne jest, skąd pochodzi. Jest balansowanie na temat liczby niestandardowych wyjątków, które chcesz dodać, ponieważ nie chcesz niepotrzebnie mnożyć jednostek (Razor Razam). Często wystarczy kilka niestandardowych wyjątków, aby zasygnalizować „ten moduł nie otrzymuje właściwych danych!” lub „ten moduł nie może robić tego, co powinien!” w sposób konkretny i dostosowany, ale nie tak zbyt precyzyjny, że trzeba ponownie wdrożyć całą hierarchię wyjątków. Często przychodzę do tego niewielkiego zestawu niestandardowych wyjątków, zaczynając od wyjątków giełdowych, a następnie skanując kod i zdając sobie sprawę, że „te N miejsc podnosi wyjątki giełdowe, ale sprowadzają się do wyższego poziomu, że nie otrzymują danych potrzebują; niech „

Jonathan Eunice
źródło
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Wtedy nie stworzyłeś kodu, który ma krytyczne znaczenie dla wydajności. Pracuję teraz nad czymś, co daje 37 mln operacji na sekundę z wkompilowanymi twierdzeniami i 42 mln bez nich. Nie ma tam potwierdzeń zewnętrznych, są one po to, aby upewnić się, że kod jest poprawny. Moi klienci są bardziej niż zadowoleni z podwyżki o 13%, kiedy jestem zadowolony, że moje rzeczy nie są zepsute.
Blrfl,
Unikaj posypywania bazy kodu niestandardowymi wyjątkami, ponieważ chociaż może ci ona pomóc, nie pomaga innym, którzy muszą się z nimi zapoznać. Zwykle, jeśli zdarza się, że rozszerzasz powszechny wyjątek i nie dodajesz żadnych członków ani funkcji, tak naprawdę nie potrzebujesz go. Nawet w twoim przykładzie PersonArgumentExceptionnie jest tak jasne jak IllegalArgumentException. Ten ostatni jest powszechnie znany z rzucania, gdy przekazywany jest nielegalny argument. Oczekiwałbym, że ten pierwszy zostanie wyrzucony, jeśli a byłby Personw nieprawidłowym stanie dla połączenia (podobnie jak InvalidOperationExceptionw C #).
Selali Adobor
To prawda, że ​​jeśli nie jesteś ostrożny, możesz wywołać ten sam wyjątek z innej funkcji, ale jest to wada kodu, a nie natury częstych wyjątków. I tu właśnie pojawiają się ślady debugowania i stosu. Jeśli próbujesz „opisać” swoje wyjątki, skorzystaj z istniejących udogodnień, które zapewniają najczęstsze wyjątki, na przykład za pomocą konstruktora komunikatów (właśnie po to jest). Niektóre wyjątki dostarczają nawet konstruktorom dodatkowych parametrów, aby uzyskać nazwę problematycznego argumentu, ale możesz zapewnić temu samemu urządzeniu dobrze sformułowany komunikat.
Selali Adobor
Przyznaję, że sama rebranding wspólnego wyjątku od prywatnej marki w zasadzie tego samego wyjątku jest słabym przykładem i rzadko wart wysiłku. W praktyce staram się emitować niestandardowe wyjątki na wyższym poziomie semantycznym, bardziej ściśle związane z intencją modułu.
Jonathan Eunice,
Wykonałem pracę wrażliwą na wydajność w obszarach numerycznych / HPC i OS / middleware. Wzrost wydajności o 13% to niemała rzecz. Mogę wyłączyć niektóre kontrole, aby je zdobyć, w taki sam sposób, w jaki dowódca wojskowy może poprosić o 105% nominalnej mocy reaktora. Ale widziałem, że „w ten sposób będzie działać szybciej”, często podawany jako powód do wyłączenia kontroli, kopii zapasowych i innych zabezpieczeń. Zasadniczo opiera się na odporności i bezpieczeństwie (w wielu przypadkach, w tym integralności danych) w celu zwiększenia wydajności. To, czy warto, jest wezwaniem do oceny.
Jonathan Eunice,
3

Jak najszybsze niepowodzenie jest świetne podczas debugowania aplikacji. Pamiętam konkretny błąd segmentacji w starszym programie C ++: miejsce, w którym wykryto błąd, nie miało nic wspólnego z miejscem, w którym zostało wprowadzone (wskaźnik zerowy był z radością przenoszony z jednego miejsca do drugiego w pamięci, zanim w końcu spowodował problem ). W takich przypadkach ślady stosu nie mogą ci pomóc.

Więc tak, programowanie defensywne to naprawdę skuteczne podejście do szybkiego wykrywania i naprawiania błędów. Z drugiej strony można go przesadzić, szczególnie przy zerowych referencjach.

W twoim konkretnym przypadku, na przykład: jeśli którekolwiek z odniesień ma wartość NULL, NullReferenceExceptionzostanie ono wyrzucone przy następnym stwierdzeniu, gdy spróbujesz uzyskać wiek jednej osoby. Naprawdę nie musisz tutaj samodzielnie sprawdzać: pozwól systemowi bazowemu wychwycić te błędy i wyrzucić wyjątki, dlatego one istnieją .

Dla bardziej realistycznego przykładu możesz użyć assertinstrukcji, które:

  1. Są krótsze do pisania i czytania:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Są specjalnie zaprojektowane dla twojego podejścia. W świecie, w którym istnieją zarówno twierdzenia, jak i wyjątki, można je rozróżnić w następujący sposób:

    • Asercje dotyczą błędów programistycznych („/ * to nigdy nie powinno się zdarzyć * /”),
    • Wyjątek stanowią przypadki narożne (wyjątkowe, ale prawdopodobne sytuacje)

W ten sposób ujawniając założenia dotyczące danych wejściowych i / lub stanu aplikacji za pomocą asercji, następny programista może lepiej zrozumieć cel Twojego kodu.

Analizator statyczny (np. Kompilator) również może być szczęśliwszy.

Wreszcie, asercje można usunąć z wdrożonej aplikacji za pomocą jednego przełącznika. Ale ogólnie rzecz biorąc, nie spodziewaj się poprawy dzięki temu: kontrole stwierdzeń w czasie wykonywania są pomijalne.

rdzeń rdzeniowy
źródło
1

O ile wiem, różni programiści preferują jedno lub drugie rozwiązanie.

Pierwsze rozwiązanie jest zwykle preferowane, ponieważ jest bardziej zwięzłe, w szczególności nie trzeba ciągle sprawdzać tego samego stanu w różnych funkcjach.

Znajduję drugie rozwiązanie, np

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

bardziej solidne, ponieważ

  1. Wyłapuje błąd tak szybko, jak to możliwe, tj. Kiedy registerPerson()jest wywoływany, a nie kiedy wyjątek wskaźnika zerowego jest rzucany gdzieś w dół stosu wywołań. Debugowanie staje się znacznie łatwiejsze: wszyscy wiemy, jak daleko niepoprawna wartość może przejść przez kod, zanim przejdzie on jako błąd.
  2. Zmniejsza sprzężenie między funkcjami: registerPerson()nie przyjmuje żadnych założeń, które inne funkcje ostatecznie wykorzystają personargument i jak go wykorzystają: decyzja, która nulljest błędem, jest podejmowana i wdrażana lokalnie.

Tak więc, szczególnie jeśli kod jest raczej złożony, wolę to drugie podejście.

Giorgio
źródło
1
Podanie przyczyny („Input input is null.”) Również pomaga zrozumieć problem, w przeciwieństwie do sytuacji, gdy zawodzi jakaś niejasna metoda w bibliotece.
Florian F
1

Zasadniczo tak, dobrym pomysłem jest „wczesne niepowodzenie”. Jednak w twoim konkretnym przykładzie wyrażenie jawne IllegalArgumentExceptionnie zapewnia znaczącej poprawy w porównaniu z NullReferenceException- ponieważ oba obsługiwane obiekty są już dostarczane jako argumenty do funkcji.

Spójrzmy jednak na nieco inny przykład.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Gdyby w konstruktorze nie było sprawdzania argumentu, dostaniesz komunikat NullReferenceExceptionpodczas wywołania Calculate.

Ale zepsuty fragment kodu nie był ani Calculatefunkcją, ani jej odbiorcą Calculate. Zepsuty fragment kodu to kod, który próbuje skonstruować PersonCalculatorwartość zerową Person- dlatego chcemy, aby wystąpił wyjątek.

Jeśli usuniemy tę jawną kontrolę argumentów, będziesz musiał dowiedzieć się, dlaczego NullReferenceExceptionwystąpił w momencie Calculatewywołania. A śledzenie, dlaczego obiekt został skonstruowany z nullosobą, może być trudne, szczególnie jeśli kod konstruujący kalkulator nie jest zbliżony do kodu, który faktycznie wywołuje Calculatefunkcję.

Pete
źródło
0

Nie w podanych przez ciebie przykładach.

Jak mówisz, rzucanie wprost nie daje ci wiele, gdy wkrótce otrzymasz wyjątek. Wielu będzie argumentować, że wyraźny wyjątek z dobrą wiadomością jest lepszy, chociaż nie zgadzam się. W wstępnie wydanych scenariuszach ślad stosu jest wystarczająco dobry. W scenariuszach po wydaniu witryna wywołująca często zapewnia lepsze komunikaty niż wewnątrz funkcji.

Drugi formularz zawiera zbyt wiele informacji dla funkcji. Ta funkcja niekoniecznie musi wiedzieć, że inne funkcje wyrzucą wartość zerową. Nawet jeśli teraz wprowadzają wartość zerową , refaktoryzacja staje się bardzo kłopotliwa, jeśli przestanie tak być, ponieważ kontrola zerowa jest rozłożona na cały kod.

Ale ogólnie rzecz biorąc, powinieneś rzucać wcześnie, gdy stwierdzisz, że coś poszło nie tak (przy poszanowaniu SUCHOŚCI). Nie są to jednak świetne przykłady.

Telastyn
źródło
-3

W twojej przykładowej funkcji wolałbym, żebyś nie sprawdzał i po prostu pozwolił na NullReferenceExceptionto.

Po pierwsze, i tak nie ma sensu przekazywać wartości null, więc natychmiast rozwiążę problem na podstawie rzucenia znaku NullReferenceException.

Po drugie, jeśli każda funkcja zgłasza nieco inne wyjątki w zależności od tego, jaki rodzaj oczywistych nieprawidłowych danych wejściowych został podany, to wkrótce dostępne są funkcje, które mogą generować 18 różnych typów wyjątków, a wkrótce okazuje się, że to zbyt dużo pracy, z którą trzeba sobie poradzić, i po prostu i tak tłumiąc wszystkie wyjątki.

Nic nie możesz naprawdę zrobić, aby pomóc w wystąpieniu błędu w czasie projektowania w Twojej funkcji, więc po prostu pozwól, aby wystąpił błąd bez zmiany.

Jaka jest nazwa?
źródło
Pracuję od kilku lat na podstawie kodu opartego na tej zasadzie: wskaźniki są po prostu usuwane z odnośników, gdy są potrzebne i prawie nigdy nie są sprawdzane pod kątem wartości NULL i są obsługiwane jawnie. Debugowanie tego kodu zawsze było bardzo czasochłonne i kłopotliwe, ponieważ wyjątki (zrzuty pamięci) występują znacznie później niż w punkcie, w którym występuje błąd logiczny. Zmarnowałem dosłownie tygodnie, szukając pewnych błędów. Z tego powodu wolę styl „jak najszybciej”, przynajmniej w przypadku złożonego kodu, w którym nie jest od razu oczywiste, skąd pochodzi wyjątek o wartości zerowej.
Giorgio
„Po pierwsze, i tak nie ma sensu przekazywanie wartości null, więc natychmiast rozwiążę ten problem na podstawie zgłoszenia wyjątku NullReferenceException.”: Niekoniecznie, jak pokazuje drugi przykład Prog.
Giorgio
„Po drugie, jeśli każda funkcja zgłasza nieco inne wyjątki w zależności od tego, jaki rodzaj oczywistych nieprawidłowych danych wejściowych został podany, to wkrótce dostępne są funkcje, które mogą generować 18 różnych typów wyjątków ...”: W tym przypadku może używać tego samego wyjątku (nawet NullPointerException) we wszystkich funkcjach: ważne jest, aby rzucać wcześnie, a nie rzucać inny wyjątek dla każdej funkcji.
Giorgio
Do tego służą wyjątki RuntimeExceptions. Każdy warunek, który „nie powinien się zdarzyć”, ale uniemożliwia działanie kodu, powinien go wyrzucić. Nie musisz ich deklarować w podpisie metody. Ale w pewnym momencie musisz je złapać i zgłosić, że działanie żądanej akcji nie powiodło się.
Florian F
1
Pytanie nie określa języka, więc opieranie się na np. RuntimeExceptionNiekoniecznie jest prawidłowym założeniem.