Menu

try catch w C#: obsługa wyjątków z catch, when i finally

try, catch i finally to sposób obsługi wyjątków w C#. Dowiedz się, jak wyjątek wędruje w górę stosu wywołań, jak łapać konkretne typy wyjątków we właściwej kolejności, filtrować przez when, sprzątać w finally, rzucać ponownie przez throw; bez utraty śladu stosu i których wyjątków nie łapać.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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ładnikCo zawiera
MessageCzytelny dla człowieka opis
GetType().NameTyp wyjątku, na przykład FormatException
StackTraceŁańcuch wywołań metod w miejscu rzucenia
InnerExceptionWyją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ątekTypowa przyczyna
NullReferenceExceptionWywołanie składnika na referencji null
ArgumentNullExceptionMetoda dostała null tam, gdzie potrzebuje wartości
ArgumentOutOfRangeExceptionArgument albo indeks listy jest poza dozwolonym zakresem
IndexOutOfRangeExceptionIndeks tablicy jest poza jej granicami
FormatExceptionint.Parse, DateTime.Parse i podobne na tekście w złym formacie
InvalidCastExceptionJawne rzutowanie na typ, którym obiekt nie jest
InvalidOperationExceptionObiekt jest w złym stanie dla wywołania (pusta sekwencja, zmieniona kolekcja)
KeyNotFoundExceptionOdczyt brakującego klucza słownika przez indeksator
DivideByZeroExceptionDzielenie liczby całkowitej albo decimal przez zero
OverflowExceptionSparsowana liczba, konwersja checked albo wynik arytmetyki checked nie mieści się w typie
FileNotFoundException, IOExceptionProblemy 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 Exception tylko na zewnętrznej krawędzi: w Main, w obsłudze żądania, w pętli wątku roboczego.
  • W spodziewanych przypadkach wybieraj TryParse, TryGetValue i sprawdzanie null zamiast wyjątków. Rzucanie jest wolne w porównaniu ze sprawdzeniem, a blok try, 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żywaj throw;.
  • 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 TryParse zamiast łapać FormatException.
  • Pokazywanie użytkownikom e.Message wyją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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ