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.