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ść null
tej 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 null
poruszać się po programie, powodując problemy w innym miejscu.
Masz rację, w tym przypadku po null
prostu spowodowałoby to NullPointerException
od 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 null
parametr 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, null
odniesienia 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?
źródło
Odpowiedzi:
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.
źródło
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.
IllegalArgumentException
jest 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: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 „ź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.PersonArgumentException
nie jest tak jasne jakIllegalArgumentException
. Ten ostatni jest powszechnie znany z rzucania, gdy przekazywany jest nielegalny argument. Oczekiwałbym, że ten pierwszy zostanie wyrzucony, jeśli a byłbyPerson
w nieprawidłowym stanie dla połączenia (podobnie jakInvalidOperationException
w C #).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,
NullReferenceException
zostanie 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ć
assert
instrukcji, które:Są krótsze do pisania i czytania:
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:
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.
źródło
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
bardziej solidne, ponieważ
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.registerPerson()
nie przyjmuje żadnych założeń, które inne funkcje ostatecznie wykorzystająperson
argument i jak go wykorzystają: decyzja, któranull
jest 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.
źródło
Zasadniczo tak, dobrym pomysłem jest „wczesne niepowodzenie”. Jednak w twoim konkretnym przykładzie wyrażenie jawne
IllegalArgumentException
nie zapewnia znaczącej poprawy w porównaniu zNullReferenceException
- ponieważ oba obsługiwane obiekty są już dostarczane jako argumenty do funkcji.Spójrzmy jednak na nieco inny przykład.
Gdyby w konstruktorze nie było sprawdzania argumentu, dostaniesz komunikat
NullReferenceException
podczas wywołaniaCalculate
.Ale zepsuty fragment kodu nie był ani
Calculate
funkcją, ani jej odbiorcąCalculate
. Zepsuty fragment kodu to kod, który próbuje skonstruowaćPersonCalculator
wartość zerowąPerson
- dlatego chcemy, aby wystąpił wyjątek.Jeśli usuniemy tę jawną kontrolę argumentów, będziesz musiał dowiedzieć się, dlaczego
NullReferenceException
wystąpił w momencieCalculate
wywołania. A śledzenie, dlaczego obiekt został skonstruowany znull
osobą, może być trudne, szczególnie jeśli kod konstruujący kalkulator nie jest zbliżony do kodu, który faktycznie wywołujeCalculate
funkcję.źródło
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.
źródło
W twojej przykładowej funkcji wolałbym, żebyś nie sprawdzał i po prostu pozwolił na
NullReferenceException
to.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.
źródło
RuntimeException
Niekoniecznie jest prawidłowym założeniem.