Wyjątki

Adam Boduch

Żaden program nie jest pozbawiony błędów - jest to zupełnie naturalne, gdyż nawet największe firmy, zatrudniające wielu programistów nie są w stanie zlikwidować w swoich produktach wszystkich niedociągnięć (dotyczy to zwłaszcza dużych projektów). Programując w Delphi, mamy możliwość - przynajmniej do pewnego stopnia - zapanowania nad tymi błędami. Błąd może bowiem wynikać z wykonania pewnej operacji, której my, projektanci, się nie spodziewaliśmy. Może też wystąpić wówczas, gdy użytkownik wykona czynności nieprawidłowe dla programu - np. poda złą wartość itp. W takim przypadku program generuje tzw. wyjątki, czyli komunikaty o błędach. My możemy jedynie odpowiednio zareagować na zaistniały wyjątek, np. poprzez wyświetlenie stosownego komunikatu czy chociażby wykonanie pewnej czynności.

Słowo kluczowe try..except

Objęcie danego kodu "kontrolą błędów" odbywa się poprzez umieszczenie go w bloku try..except. Wygląda to tak:

try
  { instrukcje do wykonania }
except
  { instrukcje do wykonania w razie wystąpienia błędu }
end;
{ instrukcje do wykonania bez względu na wyjątek }

Jeżeli kod znajdujący się po słowie Try spowoduje wystąpienie błędu, program automatycznie wykona instrukcje umieszczone po słowie Except. Po zakończeniu bloku wystąpienie wyjątku nie jest przekazywane dalej. Instrukcje znajdujące się po słowie End będą wykonane, bez względu na pojawienie się wyjątku w danym bloku.

Jeżeli program jest uruchamiany bezpośrednio z Delphi (za pomocą klawisza F9), mechanizm obsługi wyjątków może nie zadziałać. Związane jest to z tym, że Delphi automatycznie kontroluje wykonywanie aplikacji i w razie błędu wyświetla stosowny komunikat oraz zatrzymuje pracę programu. Żeby temu zapobiec, trzeba wyłączyć odpowiednią opcję. W tym celu należy otworzyć menu Tools/Options, kliknąć zakładkę Debugger Options/Borland Debuggers/Language Exceptions i usunąć zaznaczenie pozycji Notify on language Exception.

Przykład: należy pobrać od użytkownika pewne dane, np. liczbę. Dzięki wyjątkom można sprawdzić, czy wartości podane w polu TextBox (biblioteka WinForms) są wartościami liczbowymi:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
begin
  try
    Convert.ToInt32(TextBox1.Text); // próba konwersji
    MessageBox.Show('Konwersja powiodła się!');
  except
    MessageBox.Show('Musisz wpisać liczbę!');
  end;
end;

Na samym początku w bloku try następuje próba konwersji tekstu do liczby (wykorzystanie klasy Convert). Jeżeli wszystko odbędzie się pomyślnie, to okienko informacyjne będzie zawierać odpowiedni tekst. Jeżeli natomiast wartość podana przez użytkownika nie będzie liczbą, zostanie wykonany wyjątek z bloku except.

Słowo kluczowe try..finally

Kolejną instrukcją do obsługi wyjątków są słowa kluczowe Try oraz Finally. W odróżnieniu od bloku Except kod znajdujący się po słowie Finally będzie wykonywany zawsze, niezależnie od tego, czy wyjątek wystąpi, czy też nie. Po zakończeniu bloku wystąpienie wyjątku jest przekazywane dalej! Instrukcje znajdujące się po słowie End mogą nie zostać wykonane - z tego powodu częstym jest zagnieżdżanie bloku Try ze słowem kluczowym Finally w szerszym bloku Except. Pozwala to zarówno na zapewnienie finalizowania wszystkich operacji, na przykład usuwania obiektów, a jednocześnie daje możliwość wykonania obsługi określonego wyjątku w określony sposób.

Konstrukcji tej używa się np. w sytuacji, gdy konieczne jest zwolnienie pamięci, a nie można zyskać pewności, czy podczas operacji nie wystąpi żaden błąd.

{ rezerwujemy pamięć }
try
  { operacje mogące stać się źródłem wyjątku }
finally
  { zwolnienie pamięci }
end;
{ instrukcje do wykonania gdy nie wystąpił wyjątek }

Instrukcje Try oraz Finally są często używane przez programistów podczas tworzenia nowych klas i zwalniania danych - oto przykład:

MojaKlasa := TMojaKlasa.Create;
try
  { jakieś operacje }
finally
  MojaKlasa.Free;
end;

Dzięki temu niezależnie od tego, czy wystąpi wyjątek czy też nie, pamięć zostanie zwolniona! Z taką konstrukcją można spotkać się bardzo często, przeglądając kody źródłowe innych programistów.

W Delphi dla .NET istnieje pewne ułatwienie, gdyż mechanizm garbage collection zapewnia bezpieczeństwo kodu - po zakończeniu wykorzystania klasy pamięć zostanie zwolniona automatycznie. Nie ulega jednak wątpliwości, że zwalnianie klasy metodą Free jest dobrym zwyczajem i powinien być praktykowany przez projektantów aplikacji.

Zagnieżdżanie wyjątków

Możliwe jest również połączenie bloków try..except z blokiem try..finally:

try
  try
    MojaKlasa := TMojaKlasa.Create;
    { operacje mogące stać się źródłem wyjątków }
  finally
    MojaKlasa.Free; // zwolnienie pamięci
  end;
  { operacje nie wykonywane przy wystąpieniu wyjątków }
except
  { komunikat informujący o wystąpieniu wyjątku }
end;
{ operacje wykonywane bez względu na wystąpienie wyjątków }

W takim przypadku w razie wystąpienia błędu w wewnętrznym bloku Try, najpierw zostanie wykonany kod po słowie Finally, a dopiero później blok Except. Operacje pomiędzy End bloku wewnętrznego a Except nie zostaną wykonane. Taka struktura ma zastosowanie, gdy istnieje potrzeba zwolnienia pamięci lub innej formy finalizowania operacji, przy jednoczesnym podtrzymaniu istnienia informacji o wyjątku.

Słowo kluczowe raise

Słowo kluczowe Raise służy do tworzenia klasy wyjątku. Brzmi to trochę niejasno, ale w rzeczywistości tak nie jest. Spójrzmy na poniższy kod:

  if Length(TextBox1.Text) = 0 then
    raise Exception.Create('Wpisz jakiś tekst w polu TextBox!');

W przypadku gdy użytkownik nic nie wpisze w polu TextBox1, zostanie wygenerowany wyjątek. Wyjątki są generowane za pomocą klasy Exception, ale o tym opowiem nieco później. Na razie należy zapamiętać, że słowo raise umożliwia generowanie wyjątków poza blokiem try..except.

Pozostawienie słowa raise samego, jak w poniższym przypadku, spowoduje wyświetlenie domyślnego komunikatu o błędzie:

try
  { jakieś funkcje }
except
  raise;
end;

Jeżeli w tym przypadku w bloku try znajdą się instrukcje, które doprowadzą do wystąpienia błędu, to słowo kluczowe raise spowoduje wyświetlenie domyślnego komunikatu o błędzie dla tego wyjątku.

Nie można jednak używać samego słowa raise poza blokiem try..except - w takim przypadku zostanie wyświetlony komunikat o błędzie: [Error] Unit1.pas(29): Re-raising an exception only allowed in exception handler.

Klasa Exception

W module SysUtils jest zadeklarowana klasa Exception (wyjątkowo bez litery T na początku), która jest klasą bazową dla wszystkich wyjątków. W rzeczywistości działa na tej zasadzie co klasa TObject oraz System.Object. Klasa System.Object jest główną klasą .NET, a TObject, korzystając z mechanizmu Class helpers, rozszerza ją o nowe możliwości.

Klasą obsługi wyjątków w .NET jest System.Exception, a w Delphi jej funkcjonalność została rozszerzona z wykorzystaniem mechanizmu class helpers i w ten sposób mamy po prostu klasę Exception.

W Delphi istnieje kilkadziesiąt klas wyjątków (wszystkie dziedziczą po klasie Exception), a każda klasa odpowiada za obsługę innego wyjątku. Przykładowo, wyjątek EConvertError występuje podczas błędów konwersji, a EDivByZero - podczas próby dzielenia liczb przez 0. Wszystko to jest związane z tzw. selektywną obsługą wyjątków, o czym będę mówił za chwilę.

W każdym razie można zadeklarować w programie własny typ wyjątku.

type
  ELowError = class(Exception);
  EMediumError = class(Exception);
  EHighError = class(Exception);

Przyjęło się już, że nazwy wyjątków rozpoczynają się od litery E - Tobie także zalecam stosowanie takiego nazewnictwa. Od mementu zadeklarowania nowego typu można generować takie wyjątki:

raise EHighError.Create('Coś strasznego! Zakończ aplikację!');

Obiekt EHighError jest zwykłą klasą dziedziczoną po Exception, należy więc także wywołać jej konstruktor. Tekst wpisany w apostrofy zostanie wyświetlony w oknie komunikatu o błędzie.

Selektywna obsługa wyjątków

Selektywna obsługa wyjątków polega na wykrywaniu rodzaju błędu i wyświetleniu stosownej informacji (lub wykonaniu jakiejś innej czynności).

try
  { instrukcje mogące spowodować błąd }
except
  on ELowError do { jakiś komunikat }
  on EHighError do { jakiś komunikat }
end;

Właśnie przedstawiłem zastosowanie kolejnego operatora języka Delphi - On. Jak widać, dzięki niemu można określić typ wyjątku i przewidzieć odpowiednią reakcję. Delphi rozróżnia kilkadziesiąt klas wyjątków, jak np. EDivByZero (błąd związany z dzieleniem przez 0), EInvalidCast (związany z nieprawidłowym rzutowaniem) czy EConvertError (związany z nieprawidłowymi operacjami konwertowania liczb oraz tekstu). Więcej można dowiedzieć się z systemu pomocy Delphi.

Zdarzenie OnException

Na próżno szukać zdarzenia OnException na liście zakładki Events inspektora obiektów. Zdarzenie OnException jest związane z całą aplikacją, a nie jedynie z formularzem - stąd znajduje się w klasie TApplication VCL.NET (nie występuje w WinForms)!

Dzięki temu zdarzeniu można przechwycić wszystkie komunikaty o błędach występujące w danej aplikacji. Jest to jednak odmienna forma zdarzenia, której nie generuje się z poziomu inspektora obiektów. Trzeba w programie napisać nową procedurę, która będzie obsługiwała zdarzenie OnException.

Deklaracja takiej procedury musi wyglądać następująco:

    procedure MyAppException(Sender: TObject; E : Exception);

Drugi parametr E zawiera wyjątek, który wystąpił w programie. Warto może wyjaśnić, dlaczego deklaracja wygląda właśnie w taki sposób. Kiedy zdarzenia były obsługiwane z poziomu inspektora obiektów - np. OnMouseMove - zawierały one specyficzne parametry dotyczące określonej sytuacji (w przypadku OnMouseMove były to współrzędne wskaźnika myszy oraz parametr Shift). Delphi nie dopuści do uruchomienia programu w przypadku, gdy procedura zdarzeniowa OnException nie będzie zawierała parametru E.

Aby rzeczywiście móc przechwytywać wyjątki zaistniałe w programie, należy wykonać jeszcze jedną czynność:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Application.OnException := MyAppException;
end;

W efekcie program będzie obsługiwał wszelkie zaistniałe wyjątki za pomocą procedury MyAppException.

Obsługa wyjątków

Mamy już procedurę, która będzie obsługiwała zdarzenie OnException, ale to jeszcze nie wszystko. Trzeba jeszcze procedurę MyAppException jakoś oprogramować i określić, jakie czynności będą wykonywane w przypadku wystąpienia wyjątków.

procedure TMainForm.MyAppException(Sender: TObject; E: Exception);
begin
{ wyświetlenie komunikatów wyjątków }
  Application.ShowException(E);
  
  if E is EHighError then // jeżeli wyjątek to EHighError...
  begin
    if Application.MessageBox('Dalsze działanie programu grozi zawieszeniem systemu. Czy chcesz kontynuować?',
    'Błąd', MB_YESNO + MB_ICONWARNING) = Id_No then Application.Terminate;
  end;
end;

Pierwszy wiersz powyższej procedury stanowi wykonanie polecenia ShowException z klasy Application. Polecenie to powoduje wyświetlenie komunikatu związanego z danym wyjątkiem.

Kolejne instrukcje stanowią już tylko przykład tego, jak można zareagować w sytuacji wystąpienia jakiegoś konkretnego błędu:

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ComCtrls;

type
  TMainForm = class(TForm)
    rgExceptions: TRadioGroup;
    btnGenerate: TButton;
    StatusBar: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure btnGenerateClick(Sender: TObject);
  private
    procedure MyAppException(Sender: TObject; E : Exception);
  public
    { Public declarations }
  end;

  ELowError = class(Exception);
  EMediumError = class(Exception);
  EHighError = class(Exception);

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

{ TMainForm }

procedure TMainForm.MyAppException(Sender: TObject; E: Exception);
begin
{ wyświetlenie komunikatów wyjątków }
  Application.ShowException(E);
  
  if E is EHighError then // jeżeli wyjątek to EHighError...
  begin
    if Application.MessageBox('Dalsze działanie programu grozi zawieszeniem systemu. Czy chcesz kontynuować?',
    'Błąd', MB_YESNO + MB_ICONWARNING) = Id_No then Application.Terminate;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ przypisanie zdarzeniu OnException procedury MyAppException }
  Application.OnException := MyAppException;
end;

procedure TMainForm.btnGenerateClick(Sender: TObject);
begin
{ odczytanie pozycji z komponentu TRadioGroup }
  case rgExceptions.ItemIndex of
    0: raise ELowError.Create('Niegroźny błąd!');
    1: raise EMediumError.Create('Niebezpieczny błąd!');
    2: raise EHighError.Create('Bardzo niebezpieczny błąd!');
  end;
end;

end.

Zamiast standardowego wyświetlenia opisu błędu w komunikacie informacyjnym (co w listingu jest efektem wykonania polecenia ShowException) jest możliwe wyświetlenie komunikatu, np. w komponencie aplikacji. Wystarczy zmodyfikować kod z listingu i w zdarzeniu MyAppException napisać:

  StatusBar.SimpleText := E.Message;

Należy zaznaczyć, że powyższy przykładowy program jest napisany dla biblioteki VCL.NET.

Zobacz też:

3 komentarzy

Wydaje mi się, że w sekcji "Zagnieżdzanie wyjątków" jest mały błąd bo konstruktor powinien być poza blokiem try/finally. Pare linijek wcześniej jest okej - co by mogło sugerować ze jest to dowolność, tymczasem umieszczenie konstruktora w bloku try/finally, w przypadku gdy to konstruktor wywali wyjątek, może spowodować kolejne wyjątki przy próbie wywołania free.

Warto też dopisać, skąd wziąść kod (klasę) błędu

Wystarczy wywołać ten błąd w środowisku Delphi, i odczytać klasę z okienka "Debugger Exception Notiication" - Project Project2.exe raised exception class TU_JEST_KOD_BLEDU :)

o ludzie... właśnie po 8 latach programowania w TP chce na delphi przejść... Co to za durne klasy i obiekty:D wolałbym znaleźć procedurke put pixel, a sam se zrobię po swojemu te obiekty hehe;)