Rozdział 7. Wyjątki i błędy w C #

> Dodaj do ulubionych

Nie cierpię, gdy coś idzie nie tak z wykonywaniem mojego programu. Programowanie nauczyło mnie jednego: jeśli ktoś jest w stanie doprowadzić do awarii Waszego programu, to ta osoba wychodzi na spryciarza, a Wy na głupków. Spójrzmy na ten przykład:

Podaj swój wiek: dwadzieścia jeden

Program prosi użytkownika o podanie wieku. Użytkownik to robi, lecz nie przy użyciu liczb. Takie zachowanie nie podoba się programowi:

Nieobsługiwany wyjątek: System.FormatException: Łańcuch wejściowy był w nieprawidłowym formacie
    at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
    at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) 
    at RobsProgram.Main(String[] args)

To niesprawiedliwe. Ktoś na tyle inteligentny, by podać swój wiek słownie może spowodować błąd programu. W tej sekcji dowiemy się, jak język C# radzi sobie z obsługą błędów i jak sprawić, by nasze programy były na ich wystąpienie bardziej odporne.

7.1. Wyjątki i metoda Parse

Metoda Parse służy do zamieniania łańcuchów na wartości liczbowe.

int age = int.Parse(ageString);

W powyższej instrukcji użyto metody int.Parse do zamiany łańcucha ze zmiennej ageString na liczbę całkowitą, która następnie zostaje umieszczona w zmiennej age.

To dobry sposób, lecz ma swoje ograniczenia. Problem z metodą Parse polega na tym, że jeśli otrzyma łańcuch zawierający nieprawidłowy tekst, to nie wie co z nim zrobić. Gdy ja w pracy spotykam się z przerastającym mnie problemem, to przekazuję go szefowi. Szef w końcu zarabia grubą kasę i płacą mu za rozwiązywanie takich zagwozdek.

W problematycznych sytuacjach metoda Parse „wyrzuca” natomiast wyjątki, które ma „złapać” inna część programu. To tak, jakby C# przesłało szefowi notatkę o treści: „Nie wiem co z tym zrobić. Może szef ma jakiś pomysł?”. Jeśli wyjątek nie zostanie przechwycony (co oznacza, że szef jest nieobecny i nie może pomóc), to wyjątek spowoduje zakończenie programu. System wykonawczy C# wyświetli szczegóły wyjątku, po czym zakończy swoje działanie, tak jak w jednym z poprzednich przykładów.

7.2. Przechwytywanie wyjątków

Chłopcy łapiący świnię

Aby metoda Parse radziła sobie z nieprawidłowo podanymi łańcuchami, możemy dodać do programu kod, który przechwyci zgłaszane przez Parse wyjątki i spróbuje naprawić zaistniały problem. To tak zwana „dynamiczna obsługa błędów” — program będzie bowiem reagował na błędy w momencie ich wystąpienia. W tym celu będziemy musieli użyć nowej konstrukcji języka C#: instrukcji try–catch. Zbudowana jest ona ze słowa try, po którym następuje blok kodu i klauzula catch. Jeśli którakolwiek z instrukcji w bloku try zgłosi wyjątek, to program uruchomi kod z klauzuli catch, aby obsłużyć ten błąd.

    int age;
    try
    {
        age = int.Parse(ageString); 
        Console.WriteLine("Dziękuję");
    } 
    catch 
    {
        Console.WriteLine("Nieprawidłowa wartość wieku"); 
    }

Przykład 17. Proste przechwytywanie wyjątków

W powyższym kodzie metoda Parse ma za zadanie przekonwertować łańcuch zawierający wiek. Parsowanie odbywa się jednak w bloku try — jeśli metoda zgłosi wyjątek, to zostanie uruchomiony kod z bloku catch, a użytkownik ujrzy komunikat o błędzie. Zauważcie, że po zgłoszeniu wyjątku nie ma powrotu do kodu z bloku try, czyli jeśli parsowanie się nie powiedzie, to program nie wyświetli napisu Dziękuję.

7.3. Obiekt wyjątku

Przekazując problem szefowi, daję mu notatkę z informacjami na ten temat. Tak samo działają wyjątki. To obiekt zawierający szczegóły dotyczące zaistniałego problemu.

Jeśli przetworzenie łańcucha jest niemożliwe, metoda Parse tworzy obiekt wyjątku, który opisuje błąd (w tym przypadku łańcuch wejściowy był podany w nieprawidłowym formacie). Powyższy program ignoruje obiekt wyjątku i jedynie rejestruje wystąpienie błędu. Możemy jednak usprawnić możliwości diagnostyczne naszego programu poprzez przechwycenie wyjątku:

int age; 
try 
{
    age = int.Parse(ageString); 
    Console.WriteLine("Dziękuję"); 
} 
catch (Exception e) 
{
    // pobierz z wyjątku komunikat o błędzie 
    Console.WriteLine(e.Message); 
}

Przykład 18 Użycie komunikatu z obiektu wyjątku

Klauzula catch przypomina teraz wywołanie metody, której parametrem jest Exception e. Tak to właśnie działa. W klauzuli catch zmienna e przyjmuje wartość wyjątku zwróconego przez metodę Parse. Typ Exception ma własność o nazwie Message, która zawiera łańcuch z opisem błędu. Jeśli wprowadzony przez użytkownika łańcuch okaże się nieprawidłowy, to program wyświetli na ekranie tekst zawarty w wyjątku:

Łańcuch wejściowy był w nieprawidłowym formacie.

Komunikat ten pochodzi ze zwróconego wyjątku. Korzystanie z komunikatu o błędzie jest przydatne, jeśli kod z bloku try może wyrzucić kilka różnych rodzajów wyjątków — dzięki temu wiadomo, co dokładnie się stało. Obiekt Exception zawiera również inne użyteczne własności.

7.4. Zagnieżdżanie wyjątków

Po zgłoszeniu wyjątku system wykonawczy, który zarządza wykonywaniem programu od strony komputera, poszuka przypisanej do tego wyjątku klauzuli catch. Jeśli program jej nie zawiera, to wyjątek zostanie przechwycony przez klauzulę catch stanowiącą część systemu wykonawczego. Kod z tej klauzuli wyświetli szczegółowe informacje dotyczące wyjątku, a następnie zakończy działanie programu. W programie można umieścić wielopoziomowe instrukcje try–catch — w przypadku pojawienia się błędu zostanie wykonana klauzula przypisana do zgłaszającego wyjątek bloku try.

try 
{
    // wyjątki z tego poziomu przechwyci 
    // „zewnętrzna” klauzula catch
    try 
    {
        // wyjątki z tego poziomu przechwyci 
        // „wewnętrzna” klauzula catch 
    } 
    catch (Exception inner) 
    {
        // to „wewnętrzna” klauzula catch 
    }
        // wyjątki z tego poziomu przechwyci 
        // „zewnętrzna” klauzula catch 
} 
catch (Exception outer) 
{
    // to „zewnętrzna” klauzula catch 
}

Powyższy kod ilustruje działanie zagnieżdżonych instrukcji try–catch. Jeśli kod z najbardziej wewnętrznego bloku zgłosi wyjątek, to system wykonawczy odszuka wewnętrzną klauzulę. Gdy zaś wykonywanie opuści ten blok, to w przypadku wyrzucenia błędu zostanie wykonana klauzula zewnętrzna.

Złota myśl programisty: nie wyłapuj wszystkiego

Ben nazywa to syndromem fana Pokemonów®, który „musi złapać je wszystkie”. Nie macie obowiązku wyłapywania wszystkich wyjątków zgłaszanych przez kod. Jeśli ktoś spróbuje za pośrednictwem Waszej metody Load wczytać nieistniejący plik, to najlepiej będzie, jeśli metoda zgłosi wyjątek typu „Nie można odnaleźć pliku”. Nie musi próbować obsłużyć tego błędu przez zwrócenie referencji zerowej czy pustego elementu. Zwrócenie symbolu zastępczego spowoduje tylko dalsze problemy, gdy ktoś spróbuje się odwołać do zwróconego pustego elementu. Im szybciej uda się ujawnić błąd, tym łatwiej będzie zidentyfikować jego przyczynę.

7.5. Dodawanie klauzuli finally

Są pewne czynności, które program musi wykonać niezależnie od tego, czy został zgłoszony wyjątek, a mianowicie: zamknąć pliki, zwolnić zasoby i ogólnie posprzątać. Wiemy jednak, że zgłoszenie wyjątku powoduje wywołanie instrukcji z klauzuli catch, a blok try zostaje opuszczony bezpowrotnie. Kod w klauzuli catch może zostać zwrócony przez wykonywaną w tym bloku metodę, a nawet sam zgłosić wyjątek. W takich wypadkach nie zostanie uruchomiona żadna porcja kodu następująca po instrukcji try–catch.

Na szczęście w C# jest na to sposób: do konstrukcji try–catch można dodać klauzulę finally. Znajdujące się wewnątrz niej instrukcje są wykonywane bez względu na to, czy w bloku try został zgłoszony wyjątek.

try 
{
    // kod, który może zgłosić wyjątek 
} 
catch (Exception outer) 
{
    // kod przechwytujący wyjątek 
} 
finally 
{
    // kod wykonywany niezależnie od tego,
    // czy wyjątek zostanie zgłoszony, czy nie 
}

W powyższym kodzie instrukcje z klauzuli finally zostaną na pewno wykonane — albo po instrukcjach z bloku try, albo tuż zanim program opuści klauzulę try.

7.6. Zgłaszanie wyjątku

Wiemy już, jak przechwytywać wyjątki, zatem pora dowiedzieć się, jak je zgłaszać. To bardzo proste:

throw new Exception("Bum!");

Powyższa instrukcja tworzy nowy wyjątek, a następnie go zgłasza. Podczas tworzenia wyjątku możemy mu przekazać łańcuch z komunikatem, który zostanie wyświetlony. W tym przypadku podałem raczej nieprzydatny komunikat „Bum!”. Jeśli kod nie jest wykonywany w obrębie konstrukcji try–catch, to zgłoszenie wyjątku może spowodować zakończenie programu.

7.7. Zasady zgłaszania wyjątków

Wyjątki najlepiej zachować na te okazje, gdy działanie programu nie może być w żaden sposób kontynuowane. Przykładowo jeśli użytkownik kalkulatora materiałów okiennych poda nieprawidłową wysokość okna, to program może po prostu wyświetlić komunikat: “Za duża wysokość” i poprosić o wprowadzenie nowej wartości. W przypadku tej kategorii błędów nie warto zatem zgłaszać wyjątku. Osobiście korzystam z wyjątków tylko w razie jakiejś katastrofy, z którą program nie może sobie poradzić i musi przekazać błąd dalej. Wam zalecam takie samo podejście.

Złota myśl programisty: planuj obsługę wyjątków

Projektując program, powinniście zastanowić się, jakie zdarzenia mogą przeszkodzić w jego wykonaniu i jak sobie z nimi poradzicie. Możecie nawet utworzyć własne typy wyjątków w oparciu o typ udostępniany w przestrzeni nazw System i użyć ich do obsługi błędów.

Autor: Rob Miles

Tłumaczenie: Joanna Liana