16. Wyjątki - część II

16. Wyjątki - część II

 Paweł Kruczkowski
Paweł Kruczkowski
00:00
09.01.2007
48633 wyświetlenia

W poprzednim tygodniu poznaliśmy podstawowe informacje na temat wyjątków w języku C# 2.0. Potrafimy już odróżniać rodzaje błędów, jakie mogą wystąpić w naszym programie. Nauczyliśmy się również używać bloku obsługi tych często nieprzewidywalnych sytuacji. Słowa: try, catch, finally czy throw nie powinny już być dla nas obce.

Dzisiaj opowiemy sobie szerzej o instrukcji catch, a także nauczymy się pisać własne wyjątki.

Jak już wiemy, instrukcja catch służy do obsługi wyjątków. Poniższy przykład jest przypomnieniem zdobytej przez nas wiedzy na temat jej definicji:

public class Wyjatki
{
    void Dodawanie(int a, int b)
    {
        System.Console.WriteLine("Początek metody: Dodawanie()");
        System.Console.WriteLine("Wynik dodawania wynosi: {0}.", a + b);
        System.Console.WriteLine("Koniec metody: Dodawanie()");
    }
 
    void Mnozenie(int a, int b)
    {
        try
        {
            System.Console.WriteLine("Początek metody: Mnozenie()");
            throw new Exception();
            System.Console.WriteLine("Koniec metody: Mnozenie()");
        }
        catch
        {
            System.Console.WriteLine("Wyjątek został przechwycony i obsłużony");
        }
    }
 
    static void Main()
    {
        Wyjatki w = new Wyjatki();
        w.Dodawanie(5, 6);
        w.Mnozenie(18, 23);
    }

W powyższym przykładzie w klasie Wyjatki zdefiniowaliśmy 2 metody odpowiedzialne za wykonywanie operacji arytmetycznych, przy czym metoda Mnozenie() zawiera dodatkowo blok try-catch. W bloku try tej metody obok wywołania statycznej metody Writeline() tworzymy egzemplarz klasy Exception i go zgłaszamy. Wyjątek ten – jak się domyślamy - zostanie następnie przechwycony przez blok catch. Blok ten w naszym przypadku jest ogólny, gdyż nie ma w nim określonego typu wyjątku, jaki ma obsługiwać (w takiej sytuacji catch przechwyci każdy zgłoszony wyjątek).

Podsumowując, po zgłoszeniu wyjątku za pomocą throw przechodzimy do instrukcji catch, która przechwytuje ten wyjątek i go w odpowiedni sposób obsługuje (w naszym przykładzie poprzez wyświetlenie napisu). Co więcej, zgłoszenie wyjątku przez funkcję powoduje, że jej wykonanie zostaje natychmiast zatrzymane, a sterowanie programem przechodzi do kodu znajdującego się właśnie w bloku catch a program nigdy nie wróci do miejsca zgłoszenia wyjątku. Dlatego też linia kodu:

System.Console.WriteLine("Koniec metody: Mnozenie()");
 

nigdy nie zostanie wykonana przez nasz program. W związku z tym po uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

Przypomnijmy sobie jeszcze metodykę przechwytywania wyjątków w funkcjach wywołujących:

public class Wyjatki
{
    void Dodawanie(int a, int b)
    {
        try
        {
            System.Console.WriteLine("Początek metody: Dodawanie()");
            System.Console.WriteLine("Wynik dodawania wynosi: {0}.", a + b);
            System.Console.WriteLine("Koniec metody: Dodawanie()");
 
            Mnozenie(10,11);
        }
        catch (Exception e)
        {
            System.Console.WriteLine(e.Message);
        }
    }
 
    void Mnozenie(int a, int b)
    {
        System.Console.WriteLine("Początek metody: Mnozenie()");

  throw new Exception("Ten program zabrania mnożenia!");                

  System.Console.WriteLine("Koniec metody: Mnozenie()");
    }
 
    static void Main()
    {
        Wyjatki w = new Wyjatki();
        w.Dodawanie(5, 6);
    }
}

Sposób przechwytywania wyjątków w funkcjach wywołujacych jest nam już bardzo dobrze znany. Powyższy przykład prezentuje taką sytuację: w metodzie Mnozenie() zgłaszamy wyjątek (tworzymy obiekt typu Exception, w konstruktorze którego umieszczamy krótką informację na jego temat) a następnie w metodzie Dodawanie() (metoda wywołująca metodę Mnozenie()) przechwytujemy ten obiekt oraz go obsługujemy (poprzez wypisanie informacji na jego temat).

W tym miejscu musimy uświadomić sobie fakt, że wyjątki są obiektami. To jest bardzo ważna informacją, którą powinniśmy sobie zapamiętać. Klasa Exception udostępnia szereg metod oraz właściwości, które są bardzo często użyteczne dla programistów. Jedną z takich właściwości jest właśnie Message, która przechowuje informacje o danym wyjątku (na przykład dlaczego został zgłoszony), którą to użyliśmy w powyższym programie. Właściwość ta jest tylko do odczytu (nie można jej modyfikować). Po uruchomieniu powyższego przykładu otrzymamy:

Na koniec omawiania instrukcji catch prześledźmy poniższy przykład:

public class Wyjatki
{
    static int Dzielenie(int a, int b)
    {
        if (a == 0)
        {
            throw new System.ArithmeticException("Niedozwolona operacja arytmetyczna!");
        }
        if (b == 0)
        {
            throw new System.DivideByZeroException("niedozwolone dzielenie przez 0!");
        }
 
        return a / b;
    }
 
    static void Main()
    {
        try
        {
            System.Console.WriteLine("Początek metody Main()");
            Dzielenie(0, 5);
            Dzielenie(5, 0);
        }
        catch(DivideByZeroException e)
        {
            System.Console.WriteLine(e.Message);
        }
 
        catch(ArithmeticException e)
        {
            System.Console.WriteLine(e.Message);
        }
        finally
        {
            System.Console.WriteLine("Koniec metody Main()");
        }
    }
}

Instrukcja catch nie musi być jedynie instrukcją ogólną (czyli taką, w której nie zdefiniowaliśmy typu wyjątku, który ma być w niej przechwycony i obsłużony). Możemy bowiem tworzyć specyficzne instrukcje catch, które obsłużą tylko pewne typy wyjątków (w naszym przykładzie takimi typami są: DivideByZeroException oraz ArithmeticException). Przy czym w takich sytuacjach bardzo ważna jest kolejność instrukcji catch. Wyjątek DivideByZeroException dziedziczy po ArithmeticException, dlatego też gdybyśmy odwrócili ich kolejność, to kompilator zgłosi ten błąd podczas kompilacji. Musimy więc pamiętać o hierarchii dziedziczenia wyjątków.

W powyższym przykładzie w metodzie Dzielenie() zdefiniowaliśmy zgłoszenie wyjątków, jeśli zostaną spełnione dwa warunki: dzielnik oraz dzielna będą liczbami 0. Metoda Main() oferuje nam ich obsługę, w zależności jaki wyjątek w danej chwili zostanie zgłoszony. Nasz przykład zgłasza w pierwszej kolejności błąd arytmetyczny, dlatego też po jego obsłudze program przechodzi do bloku finally i druga operacja dzielenia nie zostanie już nigdy wykonana, stąd otrzymamy następujące wyniki:

Drugim punktem niniejszego artykułu jest tworzenie własnych wyjątków. Wyjątki takie są bardzo często definiowane przez programistów, pomimo faktu, że często wyjątki udostępnione przez środowisko CLR są w zupełnosci wystarczające. Ale w sytuacjach, gdy chcemy obsłużyć wcześniej zdefiniowany przez nas specyficzny wyjątek, taki mechanizm jest jak najbardziej użyteczny.

Tworzenie własnych wyjątków jest mechanizmem bardzo łatwym. Klasa takich wyjątków musi dziedziczyć po klasie ApplicationException znajdującej się w przestrzeni nazw System. Poniższy przykład prezentuje sposób tworzenia własnych wyjątków:

public class MojWlasnyWyjatek : ApplicationException
{
    public MojWlasnyWyjatek(string info) : base(info)
    { }
}
   
public class Operacje
{
    static int Dzielenie(int a, int b)
    {
        if (b == 0)
        {
            throw new DivideByZeroException("Niedozwolone dzielenie przez zero!");
        }
 
        if (a == 0)
        {
            throw new MojWlasnyWyjatek("Niedozwolone dzielenie zera!");
        }
        return a / b;
    }
 
    static void Main()
    {
        try
        {
            System.Console.WriteLine("Początek metody Main()");
            Dzielenie(0, 5);
        }
        catch(MojWlasnyWyjatek m)
        {
            System.Console.WriteLine(m.Message);
        }
 
        catch(DivideByZeroException d)
        {
            System.Console.WriteLine(d.Message);
        }
        finally
        {
            System.Console.WriteLine("Wynik dzielenie wynosi: {0}", Dzielenie(25, 5));
            System.Console.WriteLine("Koniec metody Main()");
        }
    }
}

W powyższym przykładzie utworzyliśmy klasę MojWlasnyWyjatek, która dziedziczy po klasie ApplicationException. Klasa ta składa się jedynie z konstruktora, który przyjmuje komunikat w postaci łańcucha znaków, który następnie przekazywany jest do konstruktora klasy bazowej. Utworzenie takiej klasy daje nam – programistom większą elastyczność w przechwytywaniu i obsłudze wyjątków. Innymi słowy, używanie własnych wyjątków jest niekedy bardziej czytelniejszym rozwiązaniem niż pochodzące od klasy ApplicationException  wyjątki wbudowane.

Zgłaszanie własnych wyjątków jest identyczne w porównaniu ze zgłaszaniem wyjątków wbudowanych. Po skompilowaniu i uruchomieniu powyższego programu otrzymamy więc następujące wyniki:

Na koniec omówimy mechanizm ponownego zgłaszania wyjątków. Mechanizm taki jest często spotykany w sytuacjach, gdy programista chce, aby w bloku catch nie tylko obsługiwać dany wyjątek (wykonać pewne operacje naprawcze), ale również przekazać ten wyjątek wyżej, tzn. do funkcji wywołującej. Program wówczas może zgłosić ponownie ten sam wyjątek lub inny typ wyjątku. Dla tego drugiego przypadku często zachodzi potrzeba znajomości pierwotnego, oryginalnego wyjątku jaki został zgłoszony w wspomnianym bloku catch. Poniższy przykład prezentuje sposób użycia właściwości InnerException, która jest lekarstwem na powyższy problem:

public class MojWlasnyWyjatek : ApplicationException
{
    public MojWlasnyWyjatek(string info, Exception inner) : base(info, inner)
    { }
}
 
public class Operacje
{
    static void Podziel()
    {
        try
        {
            WywolajDzielenie();
        }
        catch (MojWlasnyWyjatek e)
        {
            Console.WriteLine("{0}", e.Message);
            Exception inner = e.InnerException;
            Console.WriteLine("Historia wyjątku: {0}", inner.Message);
        }
    }
 
    static void WywolajDzielenie()
    {
        try
        {
            Dzielenie(0, 5);
        }
        catch (ArithmeticException e)
        {
            throw new MojWlasnyWyjatek("Nie wolno dzielić zera!", e);
        }
    }
 
    static int Dzielenie(int a, int b)
    {
       if (a == 0)
       {
            throw new ArithmeticException("Straszny błąd!!!");
       }
 
       return a / b;
    }
 
    static void Main()
    {
        Podziel();
    }
}

Powyższy przykład przedstawia sposób użycia w naszym programie właściwości InnerException. Jest to właściwość, która pozwala na dostęp do oryginalnego wyjątku, pomimo faktu, że została wywołana przez wyjątek o innym typie niż oryginalny. Aby ten mechanizm w pełni zrozumieć musimy przeanalizować powyższy kod. Metoda  Podziel() wywołuje metodę WywolajDzielenie(), która z kolei wywołuje metodę Dzielenie() o dwóch parametrach, którymi są liczby całkowite (0 oraz 5). Ponieważ pierwszy parametr jest liczbą 0, zostaje zgłoszony wyjątek typu ArithmeticException z odpowiednią informacją („straszny błąd” J). Wyjątek ten został przechwycony „wyżej”, tzn w metodzie WywolajDzielenie(), gdyż metoda ta wywołała Dzielenie(). W bloku catch metody WywolajDzielenie() przechwytujemy powyższy wyjątek i zgłaszamy nowy (de facto jest to zdefiniowany przez nas własny wyjątek, dla którego w konstruktorze obok informacji własnej przekazujemy również informacje na temat oryginalnego wyjątku). Nasz własny wyjątek jest następnie przechwycony i obsłużony przez instrukcję catch w metodzie Podziel(). Instrukcja ta dostarcza nam nie tylko informacje o wyjątku zgłoszonym przez obiekt typu MojWlasnyWyjatek (e.Message), ale również informacje o oryginalnym wyjątku jakim jest obiekt typu ArithmeticException (inner.Message).

Po skompilowaniu i uruchomieniu powyższego programu otrzymamy następujące wyniki:

Wiedza jaką zdobyliśmy dzisiaj i tydzień temu na pewno sprawi, że nasze programy coraz częściej będą zawierać blok try – catch – finally. Obsługa wyjątków jest bowiem mechanizmem bardzo ważnym i powinna się znaleźć w każdym nietrywialnym kodzie.

Za tydzień opowiemy sobie o delegatach i zdarzeniach.


Spodobał Ci się ten artykuł? Podziel się z innymi!

Źródło:

Polecamy również w kategorii Kurs C#, cz. II