Wyjątek

Wyjątek () - mechanizm przepływu sterowania używany w procesorach 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 procesorach 
Wyjątki w procesorach są zdarzeniami, których wynikiem jest przerwanie wykonania bieżącego strumienia instrukcji i przekazanie sterowania do oprogramowania systemowego w celu programowej reakcji na zdarzenie.

Wyjątki dzielą się na synchroniczne, obsługiwane bezpośrednio po wystąpieniu, oraz asynchroniczne, których obsługa może, w zależności od bieżącego priorytetu procesora, zostać opóźniona.

Do wyjątków asynchronicznych należą przerwania. Wyjątki synchroniczne - to pułapki (traps) i błędy (faults, aborts, errors).

Przerwania mogą być generowane sprzętowo przez sterowniki urządzeń zewnętrznych oraz - w nowocześniejszych architekturach (np. ARM) - programowo przez procesor.

Pułapki są generowane przez jednostkę wykonawczą procesora w wyniku wykonania instrukcji, na końcu jej wykonania. Pułapki służą do trzech celów:
 wywołania przez program użytkowy usług systemu operacyjnego - instrukcja SVC (ARM), INT (x86), SYSCALL (x86) itp. (pułapka wywołania systemu była dawniej niezbyt fortunnie nazywana "przerwaniem programowym")
 sygnalizacji niepoprawnego z punktu widzenia programisty wykonania programu (np. pułapka przy nadmiarze operacji arytmetycznej)
 wspomagania debugowania programów (pułapka śledzenia).
Przy wystąpieniu pułapki wykonanie instrukcji, która ją spowodowała, zostaje normalnie zakończone.

Błędy mogą być generowane przez procesor lub jego otoczenie. Charakterystyczną cechą błędów jest to, że uniemożliwiają one zakończenie wykonania instrukcji, podczas której wystąpiły. Są to np:
 błąd wyrównania danych, sygnalizowany przez jednostkę wykonawczą
 błąd niezidentyfikowanej instrukcji lub niedozwolonej instrukcji
 błędy dostępu do pamięci, sygnalizowane przez jednostkę zarządzania pamięcią (stronicowania lub segmentacji).

Wyjątki w językach programowania 
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 
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 
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 
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 
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:

 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ż 
 Przerwanie

Przypisy 

Konstrukcje programistyczne