Wyjątek

Z Wikipedii, wolnej encyklopedii
Skocz do: nawigacja, szukaj

Wyjątek (ang. exception) jest mechanizmem przepływu sterowania używanym w mikroprocesorach oraz współczesnych językach programowania do obsługi zdarzeń wyjątkowych, a w szczególności błędów, których wystąpienie zmienia prawidłowy przebieg wykonywania programu. W momencie zajścia niespodziewanego zdarzenia generowany jest wyjątek, który musi zostać obsłużony poprzez zapamiętanie bieżącego stanu programu i przejście do procedury jego obsługi. W niektórych sytuacjach po obsłużeniu wyjątku można powrócić do wykonywania przerwanego kodu, korzystając z zapamiętanych informacji stanu. Przykładowo obsługa błędu braku strony pamięci polega najczęściej na pobraniu brakującej strony z pliku wymiany, co umożliwia kontynuowanie pracy programu, natomiast błąd dzielenia przez zero powoduje, że wykonywanie dalszych obliczeń nie ma sensu i musi zostać definitywnie przerwane.

Wyjątki w mikroprocesorach[edytuj | edytuj kod]

Współczesne mikroprocesory posiadają dwa podobne mechanizmy przerywające wykonywanie programu, różniące się jednak pochodzeniem sygnału. Pierwszy z nich to przerwania sprzętowe generowane w sposób asynchroniczny przez zewnętrzne urządzenia, a drugi – wyjątki generowane synchronicznie przez sam procesor[1]. Klasyfikacja sprzętowych wyjątków zależy od konkretnej architektury procesora. Przykładowo, w procesorach Intela istnieją trzy klasy wyjątków: niepowodzenia (ang. fault), błędy nienaprawialne (ang. abort) i pułapki (ang. traps)[1], natomiast architektura PowerPC posiada tylko dwie: krytyczne i niekrytyczne[2].

Wyjątki w językach programowania[edytuj | edytuj kod]

W językach programowania wsparcie dla wyjątków realizowane jest na poziomie składni i semantyki danego języka. Zgłoszenie sytuacji wyjątkowej możliwe jest w dowolnym miejscu kodu poprzez instrukcje zwane raise lub throw. Od ich angielskich nazw w języku polskim proces ten nazywany jest podnoszeniem lub rzucaniem wyjątku. Dla dowolnej partii kodu możliwe jest zdefiniowanie bloku obsługi, który przechwytuje (ang. catch) określone rodzaje wyjątków. Poniżej widoczna jest typowa realizacja w pseudokodzie:

operacje programu
try
   operacje programu
   jeśli wystąpiła sytuacja wyjątkowa:
      throw wyjątek
   operacje programu
catch wyjątek
   obsłuż wyjątek
end
operacje programu

W momencie wykonania instrukcji throw sterowanie przekazywane jest do bloku catch, w którym powinien być zawarty kod obsługi danego rodzaju wyjątku. Po obsłużeniu, sterowanie nie powraca już do bloku try – program wykonuje się dalej od instrukcji end, zatem dalsze operacje wewnątrz tego bloku nie będą wykonywane. Zezwala się na rzucanie wyjątków z wnętrza funkcji, a także na zagnieżdżanie bloków try. W momencie wystąpienia wyjątku sterowanie jest przekazywane do pierwszego z nich, który potrafi go obsłużyć.

Blok finally[edytuj | edytuj kod]

Istotnym problemem w obsłudze wyjątków jest to, że wewnątrz bloku try mogły zostać tymczasowo zaalokowane jakieś zasoby, które po zakończeniu wykonywania powinny zostać zwolnione. Jeśli rzucanie i przechwytywanie wyjątku zachodzi w obrębie tej samej funkcji, odpowiedni kod można umieścić za sekcją try ... catch, lecz funkcja rzuca wyjątek, który powinien przechwycić kod wywołujący, programista sam musi zadbać, by zwolnić wszystkie tymczasowe zasoby przed jego rzuceniem. Dlatego w niektórych językach wprowadzony jest dodatkowy, opcjonalny blok finally, który musi się wykonać niezależnie od tego, czy wewnątrz try został rzucony wyjątek, czy nie. Poniżej przedstawiony jest przykład w pseudokodzie ilustrujący zagadnienie:

procedura foo()
   try
      zaalokuj zasób X
      operacje programu
      jeśli wystąpiła sytuacja wyjątkowa:
         throw wyjątek
      operacje programu
   finally
      zwolnij zasób X
   end
koniec
try
   wywołaj foo()
catch wyjątek
   obsłuż wyjątek
end

Na samym początku procedury foo() alokujemy pewien zasób X, który musi zostać zwolniony przed zakończeniem jej wykonywania. Jednak w międzyczasie może zostać rzucony wyjątek, który w normalnych okolicznościach spowodowałby opuszczenie procedury i pojawienie się wycieku pamięci. Dlatego kod procedury zostaje objęty blokiem try z dołączoną klauzulą finally opisującą zwolnienie zasobów. Język programowania gwarantuje nam, że zostanie ona wykonana zarówno wtedy, gdy procedura zakończy się normalnie, jak i gdy zostanie rzucony wyjątek, który obsługiwany jest przez kod ją wywołujący.

Typy wyjątków[edytuj | edytuj kod]

Reprezentacja wyjątków jest zależna od konkretnego języka programowania. Przykładowo, w C++ wyjątkiem może być wartość dowolnego typu:

try
{
   throw 20;
}
catch(int x)
{
   cout << "Wystąpił wyjątek o kodzie " << x;
}

W Javie wyjątki mogą być wyłącznie obiektami klas rozszerzających klasę Throwable:

try
{
   throw new Exception("Informacja o błędzie");
}
catch(Exception x)
{
   System.err.println(x.getMessage());
}

Bezpieczna obsługa wyjątków[edytuj | edytuj kod]

Skuteczność obsługi błędów zależy od przyjętej strategii obsługi wyjątków. Jednym z największych wyzwań jest konieczność przekazywania informacji o wyjątkach między systemami. Aplikacje biznesowe mogą składać się z wielu niewielkich programów oraz być rozproszone pomiędzy kilka maszyn, co wymaga podjęcia decyzji czy dany wyjątek powinien być obsługiwany w ramach aktualnego procesu czy przekazany do innej części systemu.

Odporność na wyjątki[edytuj | edytuj kod]

O kodzie powiemy, że jest bezpieczny dla wyjątków (ang. exception-safe), jeśli rzucenie wyjątku w jego obrębie nie produkuje niepożądanych skutków ubocznych takich, jak wycieki pamięci, generowanie nieprawidłowego wyniku czy pozostawienie systemu w stanie niespójnym. Kod bezpieczny dla wyjątków musi spełniać niezmienniki nawet w przypadku wystąpienia błędu. Wyróżniamy kilka poziomów bezpieczeństwa[3]:

  • Przezroczystość awarii – gwarantowane jest, że dana operacja zakończy się sukcesem i wyprodukuje poprawny wynik nawet wtedy, jeśli w trakcie wykonywania pojawiła się sytuacja wyjątkowa. Operacja potrafi we własnym zakresie obsłużyć wszystkie wyjątki – nigdy nie są one przekazywane do kodu ją wywołującego. Jest to najwyższy stopień bezpieczeństwa kodu.
  • Atomowość operacji (także silne bezpieczeństwo dla wątków) – operacja albo wykona się w całości poprawnie, albo w przypadku awarii pozostawi system w stanie niezmienionym (lub przywraca go do stanu początkowego), nie powodując żadnych skutków ubocznych. Kod wywołujący dostaje informację o awarii, lecz ma zagwarantowane, że żadne dane nie zostały zmienione.
  • Podstawowe bezpieczeństwo dla wątków – w przypadku awarii operacja może mieć efekty uboczne, lecz w dalszym ciągu pozostają spełnione niezmienniki. Wartości w obrębie struktur danych mogą ulec zmianie, ale w dalszym ciągu będą one poprawne z punktu widzenia systemu.
  • Minimalne bezpieczeństwo dla wątków – awaria lub wyjątek w trakcie wykonywania operacji nie powoduje wycieków pamięci, ani błędów krytycznych, lecz może produkować nieprawidłowe dane.
  • Brak bezpieczeństwa dla wątków – brak jakichkolwiek gwarancji. Najgorszy poziom bezpieczeństwa.

Zobacz też[edytuj | edytuj kod]

Przypisy

  1. 1,0 1,1 Yariv Kaplan: Interrupts and exceptions (ang.). internals.com. [dostęp 2010-10-10].
  2. The PowerPC 405TM Core (ang.). IBM Microelectronics Division. [dostęp 2010-10-10].
  3. Alexander Romanovsky: Advances in exception handling techniques. Springer, s. 61. ISBN 978-3-540-41952-5. (ang.)