Metody realizacji języków programowania/MRJP Wykład 10

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

Wyjątki

  • Pojęcie wyjątek oznacza błąd (nietypową, niepożądaną

sytuację).

  • Obsługa wyjątków oznacza reakcję programu na wykryte błędy.
  • Funkcja, która napotkała problem zgłasza (rzuca)

wyjątek.

  • Wyjątek jest przekazywany do miejsca wywołania funkcji, gdzie

może być wyłapany i obsłużony albo przekazany wyżej. Innymi słowy poszukiwania bloku obsługi wyjątku dokonywane są połańcuchu DL.

  • Przy wychodzeniu z funkcji i bloków usuwane są obiekty automatyczne.


Składnia

Dla ustalenia uwagi przyjmiemy składnię C++ (składnia Javy jest w tej kwestii bardzo podobna)

Zgłoszenie wyjątku

 throw <wyrażenie>

Obsługa wyjątków

 try {
   <instrukcje>
 } catch(<parametr 1>) {
   <obsługa wyjątku 1>
 //...
 } catch(<parametr n>) {
   <obsługa wyjątku n>
 }


Semantyka

  • Gdy któraś z instrukcji w części try przekazała

wyjątek, przerywamy wykonanie tego ciągu i szukamy catch z odpowiednim parametrem.

  • Jeśli znajdziemy, to wykonujemy obsługę tego wyjątku, a po jej

zakończeniu instrukcje po wszystkich blokach catch.

  • Jeśli nie znajdziemy, przechodzimy do miejsca wywołania

(usuwając obiekty automatyczne bieżącej funkcji) i kontynuujemy poszukiwanie.

  • Jeśli nie znajdziemy w żadnej z aktywnych funkcji, wykonanie

programu zostanie przerwane.

Implementacja obsługi wyjątków

Obsługę wyjątków można zrealizować na wiele różnych sposobów. Bardzo istotne jest jednak to, aby narzut (dodatkowy czas wykonania programu i zużyta pamięć) przy normalnym (tj. bez wystąpienia wyjątków) wykonaniu programu był minimalny, a w miarę możności zerowy. Z tego punktu widzenia realizacje wymagające wykonania dodatkowych czynności na początku i końcu bloku try można uznać za nieefektywne.

Bliższe spojrzenie na semantykę wyjątków, ujawnia, że w momencie zgłoszenia wyjątku muszą zostać wykonane następujące czynności:

  • stwierdzenie, czy nastąpiło ono wewnątrz bloku try
  • identyfikacja aktywnych bloków try - moze być więcej niż jeden, np.
void h() 
{
  try {
    f();
    try {
      g(); // tu wyjątek
    }
    catch E1 { ... }
  }
  catch E2 { ... }
}  
  • rozpoznanie typu zgłoszonego wyjątku
  • próba dopasowania do typu wyjątku jednego z bloków catch
  • w wypadku powodzenia wykonanie tego bloku
  • w przeciwnym wypadku przekazanie wyjątku w górę DL

Identyfikacja aktywnych bloków try

Dla identyfikacji aktywnych bloków try, w czasie wykonania programu musi być dostępna informacja (struktura danych), która dla każdej instrukcji pozwoli ustalić czy i jakie bloki try ją otaczają. Jeżeli chcemy uniknąć narzutu dla 'prawidłowego' przebiegu programu, informacja taka musi być w całości wygenerowana w czasie kompilacji.

Prostą a efektywną metodą spełniającą te wymagania jest użycia tablicy indeksowanej adresami instrukcji (a raczej zakresami adresów, w przeciwnym bowiem przypadku tablica miałaby rozmiar zbliżony do całkowitej liczby instrukcji programu). Elementami tej tablicy będą listy odpowiednich bloków try, lub listy odpowiednich bloków catch

Przykład

Rozważmy przytoczony wcześniej przykład funkcji:

void h() 
{
  try {
    f();
    try {
      g(); // tu wyjątek
    }
    catch E1 { I1; }
  }
  catch E2 { I2; }
  I3;
} 

gdzie I1, I2,I3 są instrukcjami niezgłaszającymi wyjątków

Załóżmy przy tym, że wygenerowany dla niej został następujący kod maszynowy:

 0: enter
 1: call f
 2: call g
 3: jmp 7
 4: I1
 5: jmp 7
 6: I2
 7: I3
 8: leave
 9: ret
 

Tablica, o której mowa wyglądać będzie następująco

Od Do Bloki catch
1 1 C1
2 2 C1, C2
3 6 C1


Ponadto dla każdego bloku catch potrzebujemy informacji o typie obsługiwanego wyjątku, oraz adresie jego kodu:

Catch Typ Adres
C1 E1 4
C2 E2 6

Zauważmy, że obie tablice mogą łatwo zostać wygenerowane w czasie kompilacji. Uzbrojeni w nie, możemy przejść do następnego etapu obsługi wyjątku.

Dopasowanie bloku catch do typu wyjątku

W wielu językach wyjątkiem może być dowolna wartość (obiekt). Dla każdego obiektu musi zatem istnieć mozliwość stwierdzenia w czasie wykonania, czy jest on określonego typu (biorąc pod uwagę także dziedziczenie) Z tej (między innymi) przyczyny, języki wspierające wyjątki, zwykle udostępniają także informacje o typach w czasie wykonania (ang RunTime Type Information, RTTI)

Rozważmy nasz przykład poszerzony o następujące definicje:

 class E1 {};
 class E2 {};
 class E3 : public E2 {};
 class K {}; 

 void g()
 {
   K k;
   throw (new E3());
 }
 
 void h() 
 {
  try {
    f();
    try {
      g(); // zgłasza wyjątek e
    }
    catch E1 { I1; }
  }
  catch E2 { I2; }
  I3;
 } 

Wywołanie funkcji g powoduje zgłoszenie wyjątku. Nazwijmy jego wartość e. W poprzedniej fazie obsługi ustaliliśmy, że w tym momencie aktywne są bloki C1, C2. Przystępujemy zatem do dopasowania typów:

  • C1 obsługuje typ E1; czy e jest typu E1? NIE.
  • C2 obsługuje typ E2; czy e jest typu E2? TAK (jest klasy E3, która jest podklasą E2).

Wykonany powinien zostać blok C2, czyli skok pod adres 6.