Menu

Rzucanie wyjątków w C#: klauzule ochronne, wyrażenia throw, własne wyjątki

Jak i kiedy rzucać wyjątki w C#. Poznaj instrukcję throw, który wbudowany typ wyjątku pasuje do którego błędu, wyrażenia throw z ?? i ?:, metody pomocnicze ThrowIfNull oraz jak napisać własną klasę wyjątku z własnymi danymi i wyjątkiem wewnętrznym.

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

Łapanie wyjątków to połowa obsługi błędów; druga połowa to rzucanie właściwych. Dobrze dobrany wyjątek mówi wywołującemu dokładnie, co poszło nie tak i czy to jego błąd, czy wynik stanu programu. Ta strona omawia stronę rzucania; obsługę omawia try catch.

Instrukcja throw i klauzule ochronne

throw przyjmuje obiekt wyjątku. Wykonanie metody zatrzymuje się w tym miejscu, a wyjątek szuka obsługi w górę stosu wywołań. Najczęstsze zastosowanie to klauzula ochronna (guard clause): sprawdzenia na początku metody, które odrzucają niepoprawne dane, zanim zostanie wykonana jakakolwiek praca.

Wynik:

ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100

nameof(amount) daje string "amount" i pozostaje poprawne po zmianie nazwy parametru. Wyjątki argumentów zapisują go w ParamName, z którego narzędzia i logi korzystają, żeby wskazać zły argument.

Klauzule ochronne upraszczają resztę metody: za sprawdzeniami kod może założyć poprawne dane. Zgłaszają też błąd w miejscu pomyłki, zamiast pozwolić złej wartości wędrować dalej i trzy metody później wywołać mylący NullReferenceException.

Jaki typ wyjątku rzucić

Używaj wbudowanego typu, gdy opisuje sytuację; wywołujący już wiedzą, jak go obsłużyć.

SytuacjaRzuć
Wymagany argument ma wartość nullArgumentNullException
Argument jest poza dozwolonym zakresem (ujemna ilość, indeks za końcem)ArgumentOutOfRangeException
Argument jest niepoprawny w inny sposób (pusta nazwa, źle sformatowane ID)ArgumentException
Wywołanie nie jest poprawne w bieżącym stanie obiektuInvalidOperationException
Ten typ nigdy nie obsługuje tej operacji (Add w kolekcji tylko do odczytu)NotSupportedException
Metoda nie została jeszcze napisanaNotImplementedException
Obiekt został użyty po DisposeObjectDisposedException
Operacji z limitem czasu skończył się czasTimeoutException

Granica między pierwszymi trzema wierszami a InvalidOperationException zależy od tego, kto musi coś zmienić. Wyjątek argumentu mówi: "wywołaj to inaczej". InvalidOperationException mówi: "wywołanie było w porządku, ale nie teraz".

Nie rzucaj bezpośrednio Exception, SystemException ani ApplicationException: wywołujący nie mogą ich złapać, nie łapiąc przy okazji wszystkiego innego. Nie rzucaj też sam NullReferenceException, IndexOutOfRangeException ani StackOverflowException; środowisko uruchomieniowe rezerwuje je dla prawdziwych błędów w kodzie.

Wyrażenia throw

Przed C# 7 throw było tylko instrukcją. Od C# 7 może też występować jako wyrażenie w trzech miejscach, co zamienia typowe sprawdzenia w jedną linię:

Wynik:

Ana <ana@example.com>
Null: name
ArgumentException: email

Zwróć uwagę, że ArgumentNullException dziedziczy po ArgumentException, więc catch (ArgumentException) umieszczony jako pierwszy złapałby też przypadek null. Z tego samego powodu kolejność klauzul catch ma znaczenie, gdy obsługujesz oba.

ThrowIfNull i podobne (.NET 6 i nowsze)

Współczesny .NET dodaje statyczne metody pomocnicze, które piszą sprawdzenie i throw za ciebie, automatycznie przechwytując nazwę parametru:

public void Ship(Order order, int quantity, string address)
{
    ArgumentNullException.ThrowIfNull(order);                   // .NET 6
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
    ArgumentException.ThrowIfNullOrWhiteSpace(address);          // .NET 8
    ObjectDisposedException.ThrowIf(disposed, this);             // .NET 7

    // ...
}

Działają tak samo jak ręcznie napisane if i throw i sprowadzają każdą klauzulę ochronną do jednej linii. W starszych wersjach użyj formy z if pokazanej wcześniej.

Pisanie własnej klasy wyjątku

Utwórz własny typ wyjątku, gdy wywołujący muszą złapać ten konkretny błąd osobno albo gdy kod obsługi potrzebuje danych, których komunikat tekstowy dobrze nie przeniesie.

Wynik:

Cannot withdraw 25 from a balance of 15.
Short by 10

Konwencje:

  • Nazwa kończy się na Exception.
  • Klasa dziedziczy po Exception (albo po bardziej szczegółowym typie wbudowanym, gdy jest jego szczególnym przypadkiem, jak InvalidOperationException).
  • Ma trzy standardowe konstruktory: bez argumentów, z komunikatem oraz z komunikatem i wyjątkiem wewnętrznym. Własne konstruktory dodaj do nich.
  • Dodatkowe dane trafiają do właściwości tylko do odczytu, ustawianych w konstruktorze. Kod obsługi może wtedy działać na podstawie e.Requested, zamiast parsować komunikat.

Opakowywanie w wyjątek wewnętrzny

Gdy błąd niskiego poziomu powinien pojawić się jako błąd wyższego poziomu, opakuj go. Oryginał zostaje zachowany jako InnerException, więc żadna informacja nie ginie:

Wynik:

Setting 'port' must be a number, got '80a'.
Caused by: FormatException

Wywołujący operuje teraz pojęciem konfiguracji, które rozumie, a zapisanie w logu e.ToString() wypisuje cały łańcuch, łącznie z FormatException i jego śladem stosu. Opakowuj tylko wtedy, gdy dodajesz znaczenie; opakowywanie każdego wyjątku w ogólny MyAppException zmusza tylko kod obsługi do przekopywania się przez InnerException.

Rzucanie wyjątku a zwracanie wyniku

Wyjątki są dla błędów, których wywołujący nie spodziewa się podczas normalnego działania. Dla rutynowych wyników, takich jak wyszukiwanie, które często nic nie znajduje, albo dane od użytkownika, które często są niepoprawne, konwencją .NET jest wzorzec Try: zwróć bool i oddaj wartość przez parametr out.

public bool TryWithdraw(decimal amount, out string error)
{
    if (amount > Balance) { error = "Insufficient funds."; return false; }
    Balance -= amount;
    error = null;
    return true;
}

Wiele typów oferuje obie wersje: int.Parse rzuca wyjątek, int.TryParse zwraca false; dict[key] rzuca wyjątek, dict.TryGetValue zwraca false. Rzucenie wyjątku kosztuje znacznie więcej niż zwrócenie wartości, więc nie powinno leżeć na ścieżce wykonywanej tysiące razy na sekundę. Parametry out opisuje strona ref i out.

Pisanie dobrych komunikatów

Komunikat wyjątku czyta programista przeglądający log. Niech mówi, co było nie tak, i tam, gdzie to bezpieczne, podaje błędną wartość: "Quantity must be between 1 and 99, got 0." jest lepsze niż "Invalid input.". Pisz pełne zdania i nie umieszczaj w komunikatach sekretów, takich jak hasła i tokeny, bo trafiają do plików logów.

Częste błędy

  • Rzucanie samego Exception. Wywołujący nie mogą go złapać selektywnie; użyj konkretnego typu.
  • Przekazywanie komunikatu w miejsce nazwy parametru. new ArgumentNullException("name") przyjmuje nazwę parametru; komunikat jest drugi.
  • Wpisane na sztywno nazwy parametrów. Używaj nameof(param), żeby zmiany nazw ich nie psuły.
  • Własne wyjątki bez dodatkowego znaczenia. Jeśli pasuje typ wbudowany, użyj go.
  • Gubienie oryginalnego błędu przy opakowywaniu. Zawsze przekazuj go jako wyjątek wewnętrzny.

Najczęściej zadawane pytania

Jak rzucić wyjątek w C#?

Utwórz obiekt wyjątku i rzuć go: throw new ArgumentException("Amount must be positive", nameof(amount));. Wykonanie zatrzymuje się w tej linii, a wyjątek wędruje w górę stosu wywołań do najbliższego pasującego catch. Wybierz najbardziej szczegółowy wbudowany typ, który opisuje problem, albo własny typ, gdy wywołujący muszą obsłużyć ten przypadek osobno.

Jak utworzyć własny wyjątek w C#?

Utwórz klasę dziedziczącą po Exception, której nazwa kończy się na Exception, i daj jej standardowe konstruktory: bez argumentów, przyjmujący komunikat oraz przyjmujący komunikat i wyjątek wewnętrzny, każdy wywołujący odpowiedni konstruktor base(...). Dodaj właściwości tylko do odczytu dla danych, których potrzebuje kod obsługi, takich jak ID zamówienia albo saldo.

Kiedy rzucać ArgumentException, a kiedy InvalidOperationException?

Rzuć ArgumentException (albo ArgumentNullException / ArgumentOutOfRangeException), gdy wywołujący przekazał złą wartość: rozwiązaniem jest inne wywołanie metody. Rzuć InvalidOperationException, gdy argumenty są w porządku, ale obiekt jest w złym stanie dla tego wywołania, na przykład przy czytaniu z zamkniętego połączenia albo wypłacie z zamrożonego konta.

Czym jest wyrażenie throw w C#?

Od C# 7 throw można używać jako wyrażenia w trzech miejscach: po ??, jako dowolną gałąź ?: oraz jako ciało składnika z ciałem wyrażeniowym albo lambdy. Na przykład _name = name ?? throw new ArgumentNullException(nameof(name)); przypisuje albo rzuca w jednej linii.

Co robi ArgumentNullException.ThrowIfNull?

To statyczna metoda pomocnicza dodana w .NET 6: ArgumentNullException.ThrowIfNull(customer); rzuca ArgumentNullException z automatycznie uzupełnioną nazwą parametru, gdy customer ma wartość null, a w przeciwnym razie nic nie robi. Późniejsze wersje dodały podobne metody, takie jak ArgumentException.ThrowIfNullOrEmpty (.NET 7) i ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ