Jason. Cała informatyka w jednym miejscu!

Dotknęliśmy kolejnego tematu, o którym można napisać że ma wyższy "level" w złożoności (wymaga starannego rozumienia poprzednio omówionych wątków, takich jak definiowanie klas). Mianowicie, obsługa wyjątków w języku C#. Mam dla Was nie jedno, nie dwa, a TRZY nowe słowa kluczowe które poznacie pod rząd, przez wzgląd że wszystkie trzy są powiązane z tematem jaki chcę przedstawić :O. Oto istna triada przeznaczona do obsługi wyjątków: "try", "catch" i "finally" w języku C#! A co to jest wyjątek, w jakiej postaci on się pojawia, jak go można obsłużyć i po co się to uprawia, artykuł ten wszystko Wam opowie ;).

OBSŁUGA WYJĄTKÓW W JĘZYKU C# JAKO WYŻSZY STOPIEŃ TRUDNOŚCI!

Obsługa wyjątków stanowi kolejny dział przyswajanej przez Was wiedzy. Jest to temat naukowy tłumaczący zagadnienia poświęcone przewidywaniu sytuacji nieoczekiwanych (czyt. niekorzystnych) i zapobieganiu poważnym awariom programu.

ALE JAK SYTUACJA MOŻE BYĆ...NIEOCZEKIWANA?

Rozszerzam skrót myślowy. Chodzi o przypadek napotkania na problem przez jakąś metodę, która miała wykonać posłusznie swoje zadanie. Przykład jaki zwykle podaję to odczyt pliku tekstowego z podanej ścieżki. Zadaniem metody jest odczytanie zawartości i wyświetlenie jej na strumień wyjściowy. Sytuacją nieoczekiwaną byłoby nieznalezienie pliku o podanej ścieżce. Można stwierdzić, że wtedy metoda "łapie się za głowę", bo nie została zaprogramowana na okoliczność radzenia sobie w sytuacjach skrajnych. Ona nie jest człowiekiem, który sam wykombinuje skąd taki plik wytrzasnąć :). Jedyne co może zrobić to zwrócić się do aplikacji w stylu "Houston, mamy problem" :D. Jak dokładnie to się dzieje, to w dalszej części niniejszego artykułu.

Jeszcze prostszym przykładem może być dzielenie przez zero. To także jest sytuacja wyjątkowa, gdyż program nie jest w stanie poradzić sobie z wyrażeniem arytmetycznym pozbawionym sensu matematycznego. Zresztą ludzie też nie :).

Po to wymyślono mechanizm obsługi wyjątków poprzez ich tak zwane "przechwytywanie". Ma to na celu umożliwienie zaimplementowania niestandardowego zachowania programu kiedy natrafi na jakiś problem nie do przeskoczenia. Domyślnie, kiedy program natrafi na wyjątek, zostaje on oznaczony jako "nieobsłużony" a aplikacja zostaje przerwana. Dzięki wykorzystaniu wiedzy o obsłudze wyjątków, możemy sami zdefiniować co program ma zrobić kiedy wyjątek (sygnał) zostanie zgłoszony. Pomysły na implementację mogą zaczynać się od przyjemniejszego komunikatu, aż do próby "inteligentnego" rozwiązania dylematu i to zawsze lepsze niż sytuacja, w której program się brutalnie zakończy i zwróci komunikat o przerwaniu działania.

WYJĄTEK NIE ODSTAJE NICZYM OD OBIEKTÓW

Mając nadzieję, że wytłumaczyłem dosyć jasno o co biega, teraz trzeba ustalić czym dokładnie jest wyjątek od strony programowania. W rzeczywistości jest to obiekt taki jak każdy inny powstały od klasy. A dokładniej, obiekt powstały z klasy dziedziczącej od klasy "Exception", klasy bazowej wszystkich wyjątków jakie są wbudowane w C#. Należy jednak zaznaczyć, że nie korzysta się z nich tak samo, jak z obiektów które mieliście okazję ujrzeć poprzednio. To nie działa tak, że tworzy się obiekt i wywołuje się jakąś złotą rączkę w charakterze metody. Nie, nie, nie. Zaraz zobaczycie o co chodzi.

"TRY", "CATCH" I "FINALLY" W JĘZYKU C#. "TRIADA" DS. OBSŁUGI WYJĄTKÓW

Obsługa wyjątków w języku C# od strony kodowej nie jest najtrudniejsza na świecie ;). Pokażę "try", "catch" i "finally" w całej okazałości. Zaczniemy od pierwszych dwóch słówek z racji tego, iż ostatnie nie jest obowiązkowe. Aby napisać kod odpowiedzialny za przechwytywanie wyjątków, stosujemy poniższą konstrukcję (na razie najprostsza jaka obowiązuje):

try
{
	// instrukcje "ryzykowne" mogące zgłosić wyjątek
}
catch
{
	// instrukcje wykonywane WYŁĄCZNIE w przypadku zgłoszenia wyjątku
}

Jak można się przyjrzeć, oto mamy w gruncie rzeczy dwa osobne bloki dla instrukcji. Pierwsze jest zawsze "try", choćby się waliło i paliło :). W tym bloku umieszczamy te instrukcje, które mogą sobie nie poradzić z wyznaczonym im zadaniem. Warto również umieszczać tam zmienne biorące udział w ryzykownych poleceniach (mianem "ryzykowne" określa się wszystko to, co może zgłosić wyjątek). Na przykład obiekt dysponujący metodą ryzykowną, przez którą program się może wysypać. Chodzi o to, iż wówczas zmienna ta występuje tylko w obrębie bloku "try" (jest to osobny blok kodu wyznaczający osobny zasięg widoczności danych) dzięki czemu kod jest bardziej zwięzły, pod warunkiem oczywiście że zmienna nie jest potrzebna na zewnątrz. Ważne jest aby wiedzieć, że kiedy zostanie zgłoszony wyjątek, to dalsze instrukcje po tej, od której powstał problem nie są wykonywane! Pisząc metaforycznie, "urwie się" w połowie.

Tuż pod blokiem "try" zwykle znajduje się "catch". "catch" to także blok dla instrukcji, ale wykonuje się tylko, gdy dojdzie do zgłoszenia wyjątku przez instrukcje w bloku "try". W przeciwieństwie do poprzednika, po słowie "catch" zwykle występuje ścisłe zdefiniowanie typu wyjątku jaki ma zostać wykryty (klasa bazowa "Exception" bądź dowolna jej klasa potomna), choć w "CSharpie" nie ma takiego obowiązku. Tak wygląda "catch" z podstawieniem wyjątku typu "Exception", czyli "wyłapującego" wszystkie zgłaszane sytuacje:

try
{
	// instrukcje "ryzykowne" mogące zgłosić wyjątek
}
catch (Exception)
{
	// instrukcje wykonywane WYŁĄCZNIE w przypadku zgłoszenia wyjątku
}

OBSŁUGA WYJĄTKÓW W JĘZYKU C# TAK NAPRAWDĘ NIE JEST POTRZEBNA!

Wyjaśnijmy sobie jeszcze kwestię a propos "wymagalności" wstawienia takiej konstrukcji. C# nie wymaga programowania obsługi wyjątków i jakiegoś "udowadniania" kompilatorowi, że mamy świadomość, że pewna instrukcja może nie wypalić. Oto kolejny czynnik odbiegający od Javy.

Java weryfikuje konieczność umieszczenia obsługi wyjątków "patrząc" na wprowadzone instrukcje, takie jakie pisaliśmy wcześniej (bez żadnego "obudowywania"). A jak doświadczeni "Javowcy" zapewne wiedzą, język charakteryzuje się wymagalnością umieszczenia takowej klauzuli celem poinformowania kompilatora, że programista jest świadom konsekwencji. Brak bloku "try-catch" jest wtedy błędem kompilacji. Co ciekawe, zwolnione od tej reguły są tzw. "runtime exceptions", czyli wyjątki powstałe podczas działania programu. Są to takie przypadki, których kompilator nie potrafi przewidzieć na czas przed kompilacją. Przykładem byłoby dzielenie przez zero posługując się zmiennymi, gdyż mimo faktu iż takie dzielenie prosi się o sabotaż, to od strony składni jest całkowicie poprawne:

int a = 12;
int b = 0;
int division = a / b;

Wtedy byłby to wyjątek "DivideByZeroException" i obsługa błędów nie byłaby konieczna z powodu dziedziczenia od klasy "RuntimeException", ale to dotyczy tylko Javy. Zapis stałych 12 i 0 bezpośrednio jako operandy, spowodowałby błąd gdyż kompilator potrafi wtedy jeszcze "przewidzieć" katastrofę:

int division = 12 / 0;	// błąd kompilacji

Po tych wszystkich tłumaczeniach, oświadczam raz jeszcze: C# NIE WYMAGA żadnego wstawiania bloków "try-catch" w stosunku do jakichkolwiek instrukcji, które jak najbardziej mogą zgłosić wyjątek, bez względu na to czy jest to "runtime exception", czy zwykły "exception"! W Javie zwolnione są od tego obowiązku WYŁĄCZNIE wyjątki pochodzące od klasy "RuntimeException".

OKREŚL DOKŁADNIE JAKI TYP WYJĄTKU MA BYĆ OBSŁUŻONY PRZEZ "CATCH"

Zgodnie z moim wcześniejszym zdaniem, wyjątek jest obiektem powiązanym z typem "Exception". Oznacza to, że możemy na naszą korzyść wykorzystać możliwości polimorficzne i podstawić inny bardziej wyszczególniony typ zamiast głównego "Exception". I tak dla przykładu, podając szczególny typ "DivideByZeroException":

try
{
	int a = 5;
	int b = 0;
	int division = a / b;
}
catch (DivideByZeroException)
{
	Console.WriteLine("Wystąpiła próba dzielenia przez zero!");
}

program będzie przygotowany na wypisanie komunikatu o niedopuszczalnym wyrażeniu arytmetycznym. Interweniując w powyższy sposób, zapobiegamy przerwaniu programu w wyniku nieobsłużenia zgłoszonego wyjątku. Pamiętajcie! W takiej sytuacji komunikat zostanie wypisany WYŁĄCZNIE dla przypadków zgłoszenia wyjątków typu "DivideByZeroException"! To znaczy, że gdyby na przykład umieścić wewnątrz bloku "try" kod, który narażony jest na wyjście poza długość tablicy, to nie dojdzie do przechwycenia wyjątku gdyż będzie to już zupełnie inny rodzaj wyjątku ("IndexOutOfRangeException"). W sytuacji powyżej doszłoby do zgłoszenia wyjątku, oznaczenia go jako "nieobsłużonego" i...koniec pieśni :(! Istnieje jednak możliwość przechwytywania WIELU wyjątków naraz, o czym piszę dalej.

WYKORZYSTYWANIE OBIEKTU WYJĄTKU WEWNĄTRZ BLOKU "CATCH"

Zazwyczaj widzimy jeszcze nazwę, tuż po wstawionym typie wyjątku:

catch (Exception e)

Obsługa wyjątków w języku C# nie każe nam umieszczać koniecznie nazwy instancji klasy bazowej "Exception" albo którejś z jej potomnych, dopóki nie będziemy potrzebować skorzystać z jej pól lub metod. Przywołajmy poprzedni przykład i lekko go zmodyfikujmy, dodając do typu wyjątku nazwę:

try
{
	int a = 5;
	int b = 0;
	int division = a / b;
}
catch (DivideByZeroException dbze)
{
	Console.WriteLine("Wystąpiła próba dzielenia przez zero!");
}

Wówczas możemy skorzystać ze składowych jakie udostępnia nam obiekt wyjątku. W tym artykule zwrócę Waszą uwagę na dwie takie: właściwość "Message" i metodę "ToString". "Message" to właściwość o dostępie publicznym, która wypisze nam treść przyczyny wystąpienia sytuacji nieoczekiwanej "na chłopski rozum" (i nawet w naszym języku :D!). Zwraca ona łańcuch znaków "string", więc najprościej podstawić wynik do wywołania "Console.WriteLine":

Console.WriteLine(dbze.Message);

"ToString" z kolei, to jest metoda pochodząca od "matki" wszystkich obiektów jaką jest "Object". Przesłonięta wersja metody dla wyjątku także przekazuje nam powód jego zgłoszenia, tylko w nieco rozbudowanej formie, mniej więcej takiej:

Treść metody "ToString" wyjątku "DivideByZeroException"

Treść metody "ToString", przesłoniętej wersji w klasie "DivideByZeroException".

a tak wygląda formułka:

Console.WriteLine(dbze.ToString());

Przypominam o nawiasach okrągłych, jako że to jest wywołanie metody!

WIELE BLOKÓW "CATCH"

Jedna obsługa wyjątków może posiadać wiele bloków "catch", jeden po drugim. Może być tak, że dana seria instrukcji może zgłosić dwa różne rodzaje wyjątków, dajmy na to dzielenie przez zero i wyjście poza tablicę. Kiedy zostawimy to w takiej postaci:

try
{
	
}
catch (Exception)
{
	
}

to wtedy dwa różne wyjątki będą obsługiwane w taki sam sposób. W zależności od okoliczności, to może być planowane bądź niechciane zachowanie. Z reguły jednak chcemy przystosować polecenia do każdej sytuacji i dany problem obsłużyć w bardzo indywidualny sposób, aby program był poinstruowany na każdą okoliczność co ma robić. Zbierając wszystkie te informacje do kupy, jesteśmy uprawnieni do napisania takiego kodu:

try
{
	
}
catch (DivideByZeroException)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu "DivideByZeroException"
}
catch (IndexOutOfRangeException)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu "IndexOutOfRangeException"
}

Gotowe. Teraz obsługa wyjątków w języku C# będzie dotyczyć zarówno jednego przypadku, jak i drugiego. Ale nie ma przystosowania do wszystkich innych rodzajów wyjątków :O! Co wtedy?

try
{
	
}
catch (DivideByZeroException)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu "DivideByZeroException"
}
catch (IndexOutOfRangeException)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu "IndexOutOfRangeException"
}
catch (Exception)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu innego niż "DivideByZeroException" i "IndexOutOfRangeException"
}

Dodajemy kolejny "catch" aby "wyłapywał" wszystkie inne przypadki. Coś jak "else" w przypadku programowania instrukcji warunkowych. Możecie to sobie z tym skojarzyć, jeśli tak Wam będzie to łatwiej zrozumieć.

MUSISZ BYĆ TEGO ŚWIADOMY(-A)!

Uważajcie na kolejność w jakiej wprowadzacie typy wyjątków! Nie możecie tego umieszczać jak sobie chcecie. Musi to przebiegać od najbardziej szczególnego typu, do najbardziej ogólnego. Żeby zobrazować sytuację, ten przykład spowoduje błąd kompilacji:

try
{
	
}
catch (Exception)
{
	// instrukcje wykonywane w przypadku zgłoszenia DOWOLNEGO wyjątku
}
catch (DivideByZeroException)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku typu "DivideByZeroException", do których program NIGDY nie dojdzie
}

Tak, błąd kompilacji! Kompilator uczepi się faktu, że wszystkie bloki "catch" zaraz po typie "Exception" są nieosiągalne, gdyż program zawsze "schodzi" z góry na dół, tak jak przy instrukcji warunkowej. Tam również kolejność weryfikowania warunków gra istotną rolę, żeby wykonać odpowiednie instrukcje przy odpowiedniej okoliczności. Kiedy na dzień dobry damy coś "za ogólnego", to zawsze by się spełniło, bo byłby uwzględniany tylko ten fakt, że został zgłoszony wyjątek i program nie będzie interesowało już jakiego typu on jest. Bądźcie czujni, programiści ;)!

WRESZCIE O "FINALLY"!

Znów mały kalambur dla poprawienia humoru :D! "finally" to jest ta trzecia część artykułu, w którym poruszana jest obsługa wyjątków w języku C#. Wewnątrz bloku "finally" umieszczamy wszystko to, co ma zostać wykonane bez względu na rezultat. Jeżeli wszystko pójdzie dobrze, zostanie wykonane. Gdy coś się posypie, zostanie wykonane. Efekt będzie taki sam :). A oto przykład kodu:

try
{
	
}
catch (Exception)
{
	// instrukcje wykonywane w przypadku zgłoszenia wyjątku
}
finally
{
	// instrukcje wykonywane zawsze, niezależnie od przebiegu programu
}

i tyle z prezentacji!

"TRY" + "FINALLY"? TAK TEŻ MOŻNA

Kombinacja "try" + "finally" może występować w kodzie bez pretensji ze strony kompilatora:

try
{
	int a = 5;
	int b = 0;
	int result = a / b;
}
finally
{
	Console.WriteLine("Nic takiego...");
}

Co się wtedy stanie? Awaria programu z powodu nieobsłużonego wyjątku (ciężko to zrobić bez bloku "catch" :P), a zaraz po nim wypisanie komunikatu. Tak, "finally" wykona się tuż przed przerwaniem działania aplikacji.

ZAGNIEŻDŻANIE BLOKÓW DO OBSŁUGI WYJĄTKÓW

Obsługa wyjątków w języku C# może też podlegać zagnieżdżaniu i ono może mieć nieskończenie wiele wgłębień. Tak wygląda przykładowe zagnieżdżenie:

try
{
	
	
	try
	{
		try
		{
			int ax = 12;
			int bx = 0;
			int result = ax / bx;
		}
		catch (DivisionByZeroException)
		{
			Console.WriteLine("Wystąpiła próba dzielenia przez zero!");
		}
	}

	int[] numbers = new int[0];
	
	numbers[0] = 13;
}
catch (IndexOutOfRangeException)
{
	Console.WriteLine("Wystąpiło odwołanie się do argumentu wykraczającego poza zakres tablicy!");
}
Obsługa wyjątków w języku C#

Obsługa wyjątków w języku C# to pisząc jednym zdaniem, zabezpieczenie się przed nieoczekiwaną awarią programu i jego nagłym zamknięciem poprzez przechwytywanie sygnału i implementację niestandardowych poleceń do wykonania.


Język C# jest bardzo elastyczny od strony obsługi wyjątków i pozwala na różne "fiki-miki" co zaprezentowałem powyżej :). Artykuł konkluduje pierwszą część przedstawiającą temat wyjątków. Tutaj jest kontynuacja. Obsługa wyjątków musi być Wam dobrze znana, bo ona również jest elementem wiedzy podstawowej.

PODOBNE ARTYKUŁY