Gdy w czasie działania coś pójdzie nie tak (brakuje pliku, tekst nie jest liczbą, klucza nie ma w słowniku), .NET rzuca wyjątek: obiekt opisujący błąd. Wyjątek wędruje w górę stosu wywołań, aż obsłuży go blok catch. Jeśli nic go nie obsłuży, program się zatrzymuje i wypisuje błąd.
Podstawowy try catch
Otocz kod, który może zawieść, blokiem try, a błąd obsłuż w catch:
Wynik:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
Gdy int.Parse("forty-two") rzuca wyjątek, następujące po nim w bloku try Console.WriteLine jest pomijane, wykonuje się blok catch (FormatException), a pętla działa dalej. Zmienną możesz pominąć (catch (FormatException)), gdy nie potrzebujesz obiektu wyjątku.
W tym konkretnym przypadku jest lepsze narzędzie: int.TryParse(input, out int n) zwraca false zamiast rzucać wyjątek. Wyjątki są dla sytuacji, których kod się nie spodziewa; dane, które często są niepoprawne, są spodziewane, więc sprawdzaj je, zamiast łapać wyjątki.
Jak wędruje wyjątek
Wyjątek rzucony głęboko w łańcuchu wywołań zwija każdą metodę, aż znajdzie pasującą obsługę. Kod po miejscu rzucenia w każdej z tych metod się nie wykonuje.
Wynik:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf i OrderTotal nie mają catch, więc wyjątek przechodzi przez nie prosto do Main. Umieść obsługę na poziomie, który wie, co zrobić z błędem, a to często nie jest miejsce, w którym błąd wystąpił.
Łapanie konkretnych wyjątków we właściwej kolejności
Klauzula catch obsługuje swój typ i każdy typ po nim dziedziczący. Gdy klauzul jest kilka, wygrywa pierwsza pasująca, więc układaj je od najbardziej szczegółowej do najbardziej ogólnej:
Wynik:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
Ostatnie wywołanie rzuca OverflowException, którego nie obsługuje żadna ze szczegółowych klauzul, więc robi to ogólne catch (Exception e). Umieszczenie catch (Exception) na początku uczyniłoby następne klauzule nieosiągalnymi, a kompilator odrzuca to błędem CS0160.
Filtry wyjątków z when
C# 6 dodał when: warunek, który decyduje, czy klauzula catch ma zastosowanie. Jeśli jest false, środowisko uruchomieniowe szuka dalej innej obsługi, jakby tej klauzuli nie było.
Wynik:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Filtry pozwalają rozgałęziać kod według danych wewnątrz wyjątku bez łapania i ponownego rzucania. To też czysty sposób na identyczną obsługę dwóch niezwiązanych typów, jak robi trzecia klauzula. Filtr wykonuje się przed zwinięciem stosu, więc gdy żaden filtr nie pasuje, debugger albo zrzut awarii nadal pokazuje oryginalny stan.
finally: kod, który wykonuje się zawsze
Blok finally wykonuje się, gdy sterowanie opuszcza try, niezależnie od tego, czy blok się zakończył, wrócił wcześniej, czy rzucił wyjątek. To miejsce na sprzątanie. (Jedno zastrzeżenie: jeśli nigdzie nic nie złapie wyjątku, proces może się zakończyć bez jego wykonania.)
Wynik:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
W każdym przypadku "Close connection" wypisuje się, zanim wynik metody dotrze do Main: finally wykonuje się po obliczeniu wartości return, ale zanim metoda faktycznie zwróci sterowanie. try może mieć finally w ogóle bez catch, co sprząta, a jednocześnie przepuszcza wyjątek do wywołującego.
Dla obiektów implementujących IDisposable (plików, strumieni, połączeń) instrukcja using pisze to try/finally za ciebie.
Ponowne rzucanie: throw; a throw e;
Czasem blok catch zapisuje coś w logu, a potem przepuszcza wyjątek dalej. Sposób ponownego rzucenia decyduje o tym, czy ślad stosu przetrwa:
Wynik:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; traktuje wyjątek jak nowo rzucony z bloku catch, więc ramki poniżej, łącznie z metodą, w której wystąpił błąd, znikają ze śladu. Zawsze rzucaj ponownie gołym throw;. (Atrybut NoInlining jest tu tylko dlatego, że JIT może wkleić tak małą metodę do wywołującej, co ukryłoby ją w obu śladach.)
Żeby zamiast tego dodać kontekst, opakuj wyjątek w nowy i przekaż oryginał jako wyjątek wewnętrzny: throw new ConfigException("Could not start the app", e);. Wyjątek wewnętrzny zachowuje własny ślad stosu, a narzędzia do logowania wypisują cały łańcuch. Pisanie własnych typów wyjątków omawia strona rzucanie wyjątków.
Obiekt Exception
Każdy wyjątek dziedziczy po System.Exception. Najczęściej używane składniki:
| Składnik | Co zawiera |
|---|---|
Message | Czytelny dla człowieka opis |
GetType().Name | Typ wyjątku, na przykład FormatException |
StackTrace | Łańcuch wywołań metod w miejscu rzucenia |
InnerException | Wyjątek, który spowodował ten, albo null |
ToString() | Typ, komunikat, wyjątki wewnętrzne i ślad stosu razem |
Zapisuj w logu e.ToString() zamiast e.Message, gdy chcesz później zdiagnozować błąd: sam komunikat rzadko mówi, gdzie był problem. Nie pokazuj e.ToString() użytkownikom końcowym.
Typowe typy wyjątków
| Wyjątek | Typowa przyczyna |
|---|---|
NullReferenceException | Wywołanie składnika na referencji null |
ArgumentNullException | Metoda dostała null tam, gdzie potrzebuje wartości |
ArgumentOutOfRangeException | Argument albo indeks listy jest poza dozwolonym zakresem |
IndexOutOfRangeException | Indeks tablicy jest poza jej granicami |
FormatException | int.Parse, DateTime.Parse i podobne na tekście w złym formacie |
InvalidCastException | Jawne rzutowanie na typ, którym obiekt nie jest |
InvalidOperationException | Obiekt jest w złym stanie dla wywołania (pusta sekwencja, zmieniona kolekcja) |
KeyNotFoundException | Odczyt brakującego klucza słownika przez indeksator |
DivideByZeroException | Dzielenie liczby całkowitej albo decimal przez zero |
OverflowException | Sparsowana liczba, konwersja checked albo wynik arytmetyki checked nie mieści się w typie |
FileNotFoundException, IOException | Problemy z systemem plików |
NullReferenceException, IndexOutOfRangeException i InvalidCastException prawie zawsze oznaczają błąd w kodzie. Popraw kod, zamiast je łapać.
Nie połykaj wyjątków
Pusty catch ukrywa każdy błąd, łącznie z tymi, których się nie spodziewasz:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
Program działa dalej, jakby zapis się udał, a prawdziwa przyczyna ginie. Wskazówki, które utrzymują obsługę wyjątków w uczciwości:
- Łap tylko to, co umiesz obsłużyć, na poziomie, który potrafi to obsłużyć.
- Jeśli łapiesz, żeby zapisać w logu, rzuć ponownie przez
throw;, chyba że program naprawdę może kontynuować. - Łap
Exceptiontylko na zewnętrznej krawędzi: wMain, w obsłudze żądania, w pętli wątku roboczego. - W spodziewanych przypadkach wybieraj
TryParse,TryGetValuei sprawdzanienullzamiast wyjątków. Rzucanie jest wolne w porównaniu ze sprawdzeniem, a bloktry, który niczego nie rzuca, nie kosztuje prawie nic.
Częste błędy
catch (Exception)na początku. Późniejsze klauzule stają się nieosiągalne (CS0160).throw e;do ponownego rzucenia. Wymazuje oryginalny ślad stosu; używajthrow;.- Puste bloki catch. Błędy znikają; przynajmniej zapisz w logu i rzuć ponownie.
- Używanie wyjątków do sterowania przepływem. Waliduj dane przez
TryParsezamiast łapaćFormatException. - Pokazywanie użytkownikom
e.Messagewyjątku z frameworka. Treść różni się między wersjami .NET i jest napisana dla programistów.
Najczęściej zadawane pytania
Jak działa try catch w C#?
Kod, który może zawieść, trafia do bloku try. Jeśli instrukcja w nim rzuci wyjątek, reszta bloku jest pomijana, a środowisko uruchomieniowe szuka klauzuli catch, której typ pasuje do wyjątku, najpierw w bieżącej metodzie, a potem w każdej wywołującej. Wykonuje się pierwszy pasujący catch, a wykonanie jest kontynuowane za całą instrukcją try.
Czy finally w C# wykonuje się zawsze?
Prawie zawsze: po normalnym zakończeniu bloku try, po obsłużeniu wyjątku przez catch, po return lub break wewnątrz bloku oraz gdy wyjątek przechodzi przez blok w drodze do catch wyżej w stosie wywołań. Nie wykonuje się, gdy proces zakończy się wcześniej: przy zabitym procesie, Environment.FailFast, StackOverflowException, a w .NET Core i nowszych przy wyjątku, którego nic nie łapie, bo kończy on proces przed wykonaniem bloków finally.
Jak złapać wiele wyjątków w C#?
Napisz kilka klauzul catch, zaczynając od najbardziej szczegółowego typu: catch (FileNotFoundException) przed catch (IOException) przed catch (Exception). Kompilator odrzuca klauzulę, do której nigdy nie da się dojść, bo wcześniejsza już łapie jej typ. Żeby obsłużyć dwa niezwiązane typy w ten sam sposób, użyj filtra: catch (Exception e) when (e is FormatException || e is OverflowException).
Jaka jest różnica między throw a throw ex w C#?
Wewnątrz catch throw; rzuca ponownie bieżący wyjątek z oryginalnym śladem stosu. throw ex; rzuca ten sam obiekt, ale resetuje ślad stosu do bieżącej linii, więc metoda, w której błąd naprawdę wystąpił, znika ze śladu. Używaj throw; albo opakuj wyjątek: throw new MyException("context", ex);.
Czym jest filtr wyjątków w C#?
To klauzula when po catch (C# 6 i nowsze): catch (HttpRequestException e) when (e.Message.Contains("404")). Blok catch wykonuje się tylko wtedy, gdy warunek jest true; w przeciwnym razie wyjątek dalej szuka innej obsługi, jakby tej klauzuli nie było, a jego stos nie jest zwijany.
Czy łapać Exception w C#?
Tylko na krawędziach programu: na początku Main, w obsłudze żądania albo w pętli działającej w tle, gdzie zadaniem jest zapisanie błędu w logu i kontynuowanie albo czyste zakończenie. Głęboko w kodzie łap konkretne typy, które naprawdę umiesz obsłużyć, a resztę przepuszczaj dalej.