Ł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ć.
| Sytuacja | Rzuć |
|---|---|
Wymagany argument ma wartość null | ArgumentNullException |
| 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 obiektu | InvalidOperationException |
Ten typ nigdy nie obsługuje tej operacji (Add w kolekcji tylko do odczytu) | NotSupportedException |
| Metoda nie została jeszcze napisana | NotImplementedException |
Obiekt został użyty po Dispose | ObjectDisposedException |
| Operacji z limitem czasu skończył się czas | TimeoutException |
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, jakInvalidOperationException). - 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).