Dlaczego nie mamy odrzucać tych wyjątków?

111

Natknąłem się na tę stronę MSDN, która stwierdza:

Nie należy celowo zgłaszać wyjątków , wyjątków SystemException , NullReferenceException ani IndexOutOfRangeException z własnego kodu źródłowego.

Niestety nie zawraca sobie głowy wyjaśnieniem, dlaczego. Mogę odgadnąć powody, ale mam nadzieję, że ktoś bardziej autorytatywny w tej sprawie może udzielić ich wglądu.

Pierwsze dwa mają jakiś oczywisty sens, ale dwa ostatnie wydają się być tymi, które chciałbyś wykorzystać (a tak naprawdę mam).

Co więcej, czy to jedyne wyjątki, których należy unikać? Jeśli są inni, czym oni są i dlaczego ich również należy unikać?

DonBoitnott
źródło
22
Od msdn.microsoft.com/en-us/library/ms182338.aspx : Jeśli zgłosisz ogólny typ wyjątku, taki jak Exception lub SystemException w bibliotece lub frameworku, zmusza to konsumentów do przechwytywania wszystkich wyjątków, w tym nieznanych wyjątków, które robią nie wiem, jak sobie radzić.
Henrik
3
Dlaczego miałbyś zgłosić wyjątek NullReferenceException?
Rik,
5
@Rik: podobnie, do NullArgumentExceptionktórego niektórzy ludzie mogą pomylić oba.
Muzułmanin Ben Dhaou
2
Metody rozszerzenia @Rik, zgodnie z moją odpowiedzią
Marc Gravell
3
Innym, którego nie powinieneś rzucać, jestApplicationException
Matthew Watson

Odpowiedzi:

98

Exceptionjest typem podstawowym dla wszystkich wyjątków i jako taki jest strasznie niespecyficzny. Nie powinieneś nigdy zgłaszać tego wyjątku, ponieważ po prostu nie zawiera on żadnych przydatnych informacji. Wywołanie kodu przechwytującego wyjątki nie może odróżnić celowo rzuconego wyjątku (z Twojej logiki) od innych wyjątków systemowych, które są całkowicie niepożądane i wskazują rzeczywiste błędy.

Ten sam powód dotyczy również SystemException. Jeśli spojrzysz na listę typów pochodnych, zobaczysz ogromną liczbę innych wyjątków o bardzo różnej semantyce.

NullReferenceExceptioni IndexOutOfRangeExceptionsą innego rodzaju. Teraz są to bardzo specyficzne wyjątki, więc ich wyrzucenie może być w porządku. Jednak nadal nie będziesz chciał ich wyrzucać, ponieważ zwykle oznaczają one, że w twojej logice są pewne błędy. Na przykład wyjątek odwołania zerowego oznacza, że ​​próbujesz uzyskać dostęp do elementu członkowskiego obiektu, którym jest null. Jeśli taka możliwość występuje w Twoim kodzie, zawsze powinieneś jawnie sprawdzić nulli zamiast tego zgłosić bardziej przydatny wyjątek (na przykład ArgumentNullException). Podobnie IndexOutOfRangeExceptions pojawiają się, gdy uzyskujesz dostęp do nieprawidłowego indeksu (na tablicach - nie na listach). Powinieneś zawsze się upewnić, że nie robisz tego w pierwszej kolejności i najpierw sprawdź granice np. Tablicy.

Istnieje kilka innych wyjątków, takich jak te dwa, na przykład InvalidCastExceptionlub DivideByZeroException, które są generowane w przypadku określonych błędów w kodzie i zwykle oznaczają, że robisz coś źle lub nie sprawdzasz najpierw niektórych nieprawidłowych wartości. Wyrzucając je świadomie z kodu, utrudniasz po prostu kodowi wywołującemu określenie, czy zostały one wyrzucone z powodu błędu w kodzie, czy po prostu dlatego, że zdecydowałeś się użyć ich ponownie w implementacji.

Oczywiście są pewne wyjątki (hah) od tych zasad. Jeśli tworzysz coś, co może powodować wyjątek, który dokładnie pasuje do istniejącego, możesz z tego skorzystać, zwłaszcza jeśli próbujesz dopasować jakieś wbudowane zachowanie. Po prostu upewnij się, że wybrałeś bardzo konkretny typ wyjątku.

Ogólnie rzecz biorąc, jeśli nie znajdziesz (konkretnego) wyjątku, który spełnia Twoje wymagania, zawsze powinieneś rozważyć utworzenie własnych typów wyjątków dla określonych oczekiwanych wyjątków. Szczególnie podczas pisania kodu biblioteki może to być bardzo przydatne do oddzielenia źródeł wyjątków.

szturchać
źródło
3
Trzecia część nie ma dla mnie sensu. Jasne, powinieneś raczej unikać powodowania tych błędów na początku, ale kiedy np. Piszesz IListimplementację, nie jesteś w stanie wpłynąć na żądane indeksy, jest to błąd logiczny dzwoniącego, gdy indeks jest nieprawidłowy i możesz go tylko poinformować tego błędu logicznego, zgłaszając odpowiedni wyjątek. Dlaczego IndexOutOfRangeExceptionnie jest właściwe?
7
@delnan Jeśli wdrażasz IList, będziesz rzucać, ArgumentOutOfRangeExceptionjak sugeruje dokumentacja interfejsu . IndexOutOfRangeExceptiondotyczy tablic iz tego, co wiem, nie można ponownie implementować tablic.
szturchnij
2
To, co może być również nieco powiązane, NullReferenceExceptionjest zwykle zgłaszane wewnętrznie jako specjalny przypadek AccessViolationException(IIRC test jest czymś w rodzaju cmp [addr], addr, tj. Próbuje wyłuskać wskaźnik i jeśli nie powiedzie się z naruszeniem dostępu, obsługuje różnicę między NRE i AVE w wynikowym programie obsługi przerwań). Więc oprócz powodów semantycznych, w grę wchodzi również oszustwo. Może to również zniechęcić Cię do nullręcznego sprawdzania, czy nie jest to pomocne - jeśli i tak zamierzasz rzucić NRE, dlaczego nie pozwolić, aby .NET to zrobił?
Luaan,
Odnosząc się do twojej ostatniej wypowiedzi, dotyczącej niestandardowych wyjątków: nigdy nie czułem takiej potrzeby. Może czegoś mi brakuje. W jakich warunkach należałoby stworzyć niestandardowy typ wyjątku zamiast czegoś z frameworka?
DonBoitnott
1
Cóż, w przypadku mniejszych aplikacji może nie być takiej potrzeby. Ale gdy tylko utworzysz coś bardziej złożonego, w którym poszczególne części działają jako niezależne „komponenty”, często ma sens wprowadzenie niestandardowych wyjątków dla niestandardowych sytuacji błędów. Na przykład, jeśli masz jakąś warstwę kontroli dostępu i próbujesz wykonać jakąś usługę, chociaż nie masz do tego upoważnienia, możesz zgłosić niestandardowy „wyjątek odmowy dostępu” lub coś takiego. Lub jeśli masz parser, który analizuje jakiś plik, możesz mieć własne błędy parsera, aby zgłosić je użytkownikowi.
szturchnij
36

Podejrzewam, że celem dwóch ostatnich jest zapobieżenie pomyłkom z wbudowanymi wyjątkami, które mają oczekiwane znaczenie. Uważam jednak, że jeśli zachowujesz dokładny cel wyjątku : to jest właściwy throw. Na przykład, jeśli piszesz zbiór niestandardowy, użycie go wydaje się całkowicie uzasadnione IndexOutOfRangeException- jaśniejsze i bardziej szczegółowe, IMO niż ArgumentOutOfRangeException. I chociaż List<T>można wybrać to drugie, istnieje co najmniej 41 miejsc (dzięki uprzejmości reflektora) w BCL (bez tablic), które rzucają na zamówienie IndexOutOfRangeException- z których żadne nie jest na tyle „niskiego poziomu”, by zasługiwać na specjalne zwolnienie. Więc tak, myślę, że możesz słusznie argumentować, że ta wskazówka jest głupia. Również,NullReferenceException jest trochę przydatne w metodach rozszerzających - jeśli chcesz zachować semantykę, że:

obj.SomeMethod(); // this is actually an extension method

rzuca NullReferenceExceptionkiedy objjest null.

Marc Gravell
źródło
2
„jeśli zachowujesz dokładną intencję wyjątku” - z pewnością gdyby tak było, wyjątek zostałby wyrzucony bez konieczności testowania go w pierwszej kolejności? A jeśli już to przetestowałeś, to nie jest to wyjątek?
PugFugly,
5
@PugFugly zajmie 2 sekundy, aby spojrzeć na przykład metody rozszerzającej: nie, to nie zostanie wyrzucone bez konieczności przetestowania jej. Jeśli SomeMethod()nie ma potrzeby uzyskiwania dostępu do członków, wymuszanie tego jest nieprawidłowe. Podobnie: podnieś ten punkt z 41 miejscami w BCL, które tworzą niestandardowe IndexOutOfRangeException, i 16 miejscami, które tworzą niestandardoweNullReferenceException
Marc Gravell
16
Twierdziłbym, że metoda rozszerzenia powinna nadal rzucać ArgumentNullExceptionzamiast a NullReferenceException. Nawet jeśli cukier składniowy z metod rozszerzających zezwala na tę samą składnię, co zwykły dostęp do elementu członkowskiego, nadal działa bardzo różnie. Uzyskanie NRE z MyStaticHelpers.SomeMethod(obj)byłoby po prostu złe.
poke
1
@PugFugly BCL jest „biblioteką klas bazowych”, w zasadzie podstawowym elementem .NET.
szturchnij
1
@PugFugly: istnieje wiele scenariuszy, w których niepowodzenie zapobiegawczego wykrycia warunku mogłoby spowodować wyrzucenie wyjątku w „niewygodnym” czasie. Jeśli operacja nie powiedzie się, wczesne zgłoszenie wyjątku jest lepsze niż rozpoczęcie operacji, przejście w połowie, a następnie konieczność wyczyszczenia wynikowego częściowo przetworzonego bałaganu.
supercat
5

Jak wskazałeś, w artykule Tworzenie i zgłaszanie wyjątków (przewodnik programowania w języku C #) w temacie Rzeczy, których należy unikać podczas zgłaszania wyjątków , firma Microsoft rzeczywiście podaje System.IndexOutOfRangeExceptionjako typ wyjątku, który nie powinien być celowo wyrzucany z własnego kodu źródłowego.

W przeciwieństwie jednak do tego w artykule (odwołanie do języka C #) Microsoft wydaje się naruszać własne wytyczne. Oto metoda, którą Microsoft zawarł w swoim przykładzie:

static int GetNumber(int index)
{
    int[] nums = { 300, 600, 900 };
    if (index > nums.Length)
    {
        throw new IndexOutOfRangeException();
    }
    return nums[index];
}

Tak więc sam Microsoft nie jest konsekwentny, ponieważ demonstruje wyrzucanie IndexOutOfRangeExceptionw swojej dokumentacji dla throw!

To prowadzi mnie do przekonania, że ​​przynajmniej w przypadku IndexOutOfRangeException, mogą zaistnieć sytuacje, w których ten typ wyjątku może zostać rzucony przez programistę i uznany za dopuszczalną praktykę.

DavidRR
źródło
1

Kiedy przeczytałem twoje pytanie, zadałem sobie pytanie, w jakich warunkach chciałoby się wyrzucić typy wyjątków NullReferenceException, InvalidCastExceptionlub ArgumentOutOfRangeException.

Moim zdaniem, kiedy napotykam jeden z tych typów wyjątków, ja (programista) czuję się zaniepokojony ostrzeżeniem w tym sensie, że kompilator do mnie mówi. Tak więc, zezwolenie Tobie (programiście) na rzucanie takich typów wyjątków jest równoważne (kompilatorowi) sprzedanie odpowiedzialności. Na przykład sugeruje to, że kompilator powinien teraz pozwolić programiście zdecydować, czy obiekt jest null. Ale podjęcie takiej decyzji powinno być naprawdę zadaniem kompilatora.

PS: Od 2003 roku opracowuję własne wyjątki, więc mogę je rzucać, jak chcę. Myślę, że takie postępowanie jest uważane za najlepszą praktykę.

Bellash
źródło
Słuszne uwagi. Myślę jednak, że dokładniejsze byłoby stwierdzenie, że programista powinien pozwolić środowisku uruchomieniowemu .NET Framework na wyrzucanie tego typu wyjątków (i że programista powinien obsługiwać je w odpowiedni sposób).
DavidRR,
0

Odkładając dyskusję na temat NullReferenceExceptioni IndexOutOfBoundsExceptionodkładając na bok:

A co z łapaniem i rzucaniem System.Exception. Wrzuciłem ten typ wyjątku do swojego kodu i nigdy mnie to nie spieprzyło. Podobnie, bardzo często łapię niespecyficzny Exceptiontyp i też mi się to udało. Więc dlaczego tak jest?

Zwykle użytkownicy twierdzą, że powinni umieć rozróżniać przyczyny błędów. Z mojego doświadczenia wynika, że ​​jest tylko kilka sytuacji, w których chciałbyś inaczej obsługiwać różne typy wyjątków. W tych przypadkach, w których oczekujesz, że użytkownicy będą obsługiwać błędy programowo, należy zgłosić bardziej szczegółowy typ wyjątku. W innych przypadkach ogólne wytyczne dotyczące najlepszych praktyk nie przekonują mnie.

Więc jeśli chodzi o rzucanie Exception, nie widzę powodu, aby tego zabronić we wszystkich przypadkach.

EDYCJA: również ze strony MSDN:

Wyjątki nie powinny być używane do zmiany przepływu programu w ramach zwykłego wykonywania. Wyjątki powinny być używane tylko do zgłaszania i obsługi błędów.

Nadużywanie klauzul catch z indywidualną logiką dla różnych typów wyjątków również nie jest najlepszą praktyką.

uebe
źródło
Komunikat o wyjątku jest nadal na miejscu, aby powiedzieć, co się stało. Nie mogę sobie wyobrazić, że idziesz i tworzysz nowy typ wyjątku dla każdego innego błędu, na wypadek gdyby konsument chciał rozróżnić te błędy programowo.
uebe
Co się stało z moim poprzednim komentarzem?
DonBoitnott