Umíme to s Delphi, 11. díl – Běhové chyby a mechanismus výjimek

Běhové chyby mohou vzniknout v důsledku „čehosi prohnilého“ při provádění funkcí, procedur a metod pracujících v rámci knihovny vizuálních komponent, běhové knihovny či v rámci operačního systému.
Běhové chyby mohou vzniknout v důsledku „čehosi prohnilého“ při provádění funkcí, procedur a metod pracujících v rámci knihovny vizuálních komponent (Visual Component Library, VCL), běhové knihovny (Runtime Library, RTL) či v rámci operačního systému. Jakmile tušíme, že by se v daném úseku programového kódu mohla vyskytnout chyba, musíme všechny možnosti správně ošetřit (a to i v případě, že pravděpodobnost vzniku chyby v daném místě je takřka zanedbatelná, neboť Murphyho (i jiné) zákony nás učí, že může-li se něco pokazit, zaručeně se to pokazí).

Vytvoříme proceduru, na které budeme demonstrovat různé přístupy k ošetření chyb. Vytvořte formulář, přidejte na něj tlačítko (Button) a ošetřete jeho kliknutí (OnClick) takto:

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  C := A div B;
  ShowMessage(IntToStr(C));
  btnTest.Caption := IntToStr(C);
end;

Vypisujeme výsledek celočíselného podílu dvou čísel (operátor div).

V takto zapsané proceduře neošetřujeme vůbec žádné chyby. Vzhledem k tomu, co jsme si napřed přiřadili do obou proměnných, je zřejmé, že nastane chyba. Tato chyba způsobí výjimku, kterou generuje operátor div. Když si náš program spustíte a kliknete na tlačítko, uvidíte zprávu o výjimce. I když sami nic neošetřujeme, výjimka je zjevně ošetřena. Proč tomu tak je, si povíme níže.

Tradiční způsob ošetření chyb

Tradiční styl spočívá zpravidla v podmínkách a testech, většinou kontrolujeme návratové kódy a „výstupy“ procedur a funkcí a dále operandy u operací apod. Problémy (nevýhody) tohoto postupu jsou (zjednodušeně řečeno) následující:
  • musíme si příslušné návratové kódy pamatovat;
  • každá funkce indikuje „neúspěch“ jinak – vrátí false, 0, -1, apod.;
  • ještě horší je to u procedur, které vrací hodnotu zpravidla přes parametr předaný odkazem (v horším případě v globálních proměnných :-)).

Podívejme se, jak bychom tradičně ošetřili chybu u procedury z příkladu:

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  if B <> 0 then
  begin
    C := A div B;
    ShowMessage(IntToStr(C));
    btnTest.Caption := IntToStr(C);
  end
  else begin
    ShowMessage(`Chystáte se dělit nulou!`);
    btnTest.Caption := `Chyba`;
  end;
end;

Ošetření chyby s užitím výjimek

Nyní tutéž proceduru přepíšeme s užitím výjimky. Nevadí, že zatím neznáme přesnou syntaxi práce s výjimkami ani definici vlastní výjimky. K tomu se dostaneme vzápětí, nyní jde jen o demonstraci toho, jak bude procedura vypadat.

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  try
    C := A div B;
    ShowMessage(IntToStr(C));
    btnTest.Caption := IntToStr(C);

  except
    on EDivByZero do begin
      ShowMessage(`Chystáte se dělit nulou!`);
      btnTest.Caption := `Chyba`;
    end;
  end;

end;

Takto zapsaná procedura vypíše chybovou hlášku a do titulku tlačítka napíše slovo „Chyba“ jako indikaci chyby. Pokud změníte hodnotu proměnné B na nenulovou, vypíše se místo toho výsledek podílu (tedy nula) a ten bude také přiřazen do titulku tlačítka.

Tento triviální příklad demonstruje v nejhrubších rysech práci s výjimkami. Celý mechanismus výjimek je postaven na čtyřech klíčových slovech:

  • try – označuje začátek tzv. chráněného bloku kódu, tedy bloku, ve kterém se očekává vznik výjimky a který se má „zkusit“ provést;
  • except – označuje konec chráněného bloku, uvádí příkazy pro obsluhu výjimek, a to ve formátu: on {typ_výjimky} do {příkazy} else {příkazy}
  • finally – začíná blok používaný např. k uvolnění zdrojů alokovaných v bloku try předtím, než je obsloužena výjimka. Tento blok je proveden vždy, ať k výjimce dojde nebo ne!
  • raise – příkaz používaný k vyvolání výjimky. I když se zdá, že je nesmyslné výjimku vyvolávat ručně (že nám bohatě stačí výjimky od systému), opak je pravdou a občas se raise hodí.
Než si ukážeme konkrétní práci s výjimkami, řekneme si něco o výjimkách a stavu programu. Dojde-li k výjimce, hledá se „ošetřující procedura“ (tzv. handler výjimky), tedy procedura, která výjimku ošetří. Není-li nalezena v dané části programu, je výjimka „přenesena“ (tzv. propagována) výše, a to až do okamžiku, kdy se o ni někdo postará. V extrémním případě jde tento postup až k tzv. implicitnímu handleru výjimek v Delphi (proto je nakonec ošetřena každá výjimka). Důležité je, že po ošetření výjimky program pokračuje kódem následujícím po kódu handleru a nikoliv kódem následujícím po kódu, který způsobil výjimku.

Podívejme se nyní blíže na sekci finally. Ta se používá k provedení nějaké činnosti v případě výjimky i v případě normálního provedení (typicky vyčištění paměti, apod.). Kód po finally bude vykonán vždy po opuštění bloku try, ať již k výjimce došlo nebo nikoliv.

Podívejme se na příklad ošetření výjimky bez použití bloku finally (místo čištění paměti budeme měnit titulek formuláře) :

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  try
    C := A div B;
    wndHlavni.Caption := `Nazdar`;

  except
    on EDivByZero do begin
      ShowMessage(`Chystáte se dělit nulou!`);
      btnTest.Caption := `Chyba`;
    end;
  end;
end;

V případě dělení nulou nebude nikdy provedeno nastavení titulku formuláře na „Nazdar“. Řešením je užití bloku finally:

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  try
    C := A div B;

  finally
    wndHlavni.caption := `Nazdar`;
  end;
end;

Nyní máme jistotu, že nastavení titulku se vždy provede. Bohužel ale zase nemáme ošetřenou vlastní výjimku, což není úplně šikovné, protože kvůli tomu to všechno děláme. Navíc blok finally toto ošetření neumožňuje (lépe řečeno – ošetření sice umožňuje, ale výjimka se bude šířit (propagovat) dál, protože z hlediska Delphi stále ošetřena nebude). Takže nám nezbývá, než zkombinovat finally a except blok (jde vlastně o zanoření dvou bloků):

procedure wndHlavni.btnTestClick(Sender: TObject);
var
  A, B, C: Integer;

begin
  A := 0;
  B := 0;

  try
    try
      C := A div B;

    except
      on EDivByZero do begin
        showmessage(` Chystate se delit nulou!`);
        btnTest.Caption := `Chyba`;
      end;
    end;

  finally
    wndHlavni.Caption := `Nazdar`;
  end;
end;

Poznámka: pokud chcete tyto příklady přesně opisovat a zkoušet, mějte na paměti, že kompilátor vůbec nezařadí do výsledného programu příkaz c := a div b, pokud hodnotu c někde dále nepotřebujete! A nekamenujte mě prosím za nesmyslnost uvedených příkladů, jsem si jí vědom; chápejte uvedené příklady jen jako ukázku.

Syntaxe bloku except

Blok except umožňuje více možností použití:

try
  {příkazy}

except
  on {očekávaná_výjimka} do
    {ošetření této výjimky}

  on {očekávaná_výjimka} do
    {ošetření této výjimky}

  …
  else
    {ošetření jakékoliv jiné výjimky}
end;

Vidíme, že v sekci else můžeme ošetřit jakoukoliv výjimku, i tu, kterou jsme neočekávali (a tedy nezařadili do výčtu on .. do). V případě ošetřování neznámých podmínek ale buďte maximálně opatrní. Zpravidla je nejlepší nechat ošetření neznámé výjimky na implicitním handleru Delphi. Není dobrý ani nápad výjimku ošetřit (např. indikovat MessageBoxem) a následně znovu vyvolat, protože pak o ní bude uživatel informován dvakrát: vaším MessageBoxem a MessageBoxem Delphi. Takže dostáváme zlaté pravidlo:

Buď výjimku ošetříme, nebo ji necháme bez povšimnutí ošetřit standardně!

Pokud už chcete výjimku sami ošetřit, je možné například použít vyvolání nové výjimky se zadaným chybovým textem:

raise EConvertError.Create(`Nelze zkonvertovat!`);

Poznámky pro pokročilé uživatele:

  • Kdykoliv dojde k výjimce, oznámí se tato skutečnost aplikaci. Kompilátor vygeneruje kód, který ví, jak přerušit provádění aplikace. Obsah zásobníku volání se pak začíná odvíjet od místa přerušení: předávané parametry a lokální proměnné se vyzdvihnou ze zásobníku (a tím uvolní z paměti). Pokud má daná funkce proměnnou typu ukazatel, jejíž paměť je dynamicky alokována, uvolní se při odvíjení zásobníku pouze 4 bajty alokované na zásobníku pro typ ukazatel. Dynamickou paměť je nutno uvolnit ručně!
  • Proces odvíjení zásobníku pokračuje, dokud se nenalezne konstrukce except nebo globální ovladač výjimek.
  • Při vyvolání výjimky se vytvoří objekt chyby, který zůstává v paměti až do uvolnění. Tento objekt se nazývá „instance objektu výjimka“. Konstrukce finally neuvolňuje z paměti tuto instanci, proto výjimku v podstatě „neošetří“.
  • Globální ovladač podmínek je definován v objektu Application, což je instance třídy TApplication, která je definovaná v jednotce Forms. Tento objekt slouží k různým účelům, a jedním z nich je spuštění události OnException v případě, že v programu dojde k výjimce.

Příště – standardní dialogy, práce se soubory

Za týden se podíváme na práci se soubory a v souvislosti s tím na práci se standardními dialogy (otevření a zavření souboru, výběr typu písma, výběr barvy, tisk…).
Diskuze (9) Další článek: VIA KT133A čipset má další chybu

Témata článku: , , , , , , , , , , , , ,