Vznik chyby je v Delphi je obsloužen vytvořením výjimky. Výjimka je objekt, který obsahuje informaci o tom, jaká chyba se stala a kde vznikla.
Reakce na výjimku je vždy vázána na programový blok. Potřebujeme-li tedy pro posloupnost programových příkazů určitou reakci na chybu, uzavřeme tyto příkazy do bloku a definujeme reakci na chybu pro tento blok.
Bloky s definovanou reakcí na výjimky se nazývají chráněné bloky, protože jsou částečně chráněny proti chybám, které jinak mohou způsobit havárii aplikace nebo poškodit data. Chráněný blok začíná klíčovým slovem try a končí klíčovým slovem end.
Ukončující kód používáme většinou k tomu, aby aplikace i přes výskyt chyby uvolnila přidělené systémové prostředky. Bližší vysvětlení je v následující části "Ochrana přidělených zdrojů".
Většinou definujeme obsluhu výjimek tak, abychom umožnili zotavení aplikace ze vzniklé chyby a její pokračování. Výjimky, které lze takto obsloužit, jsou pokusy otevřít neexistující soubor, zápis na plný disk nebo výpočty, které vybočily ze stanovených mezí. Některé z nich (např. nenalezení souboru) lze odstranit snadno, zatímco odstranění jiných (např. vyčerpání volné paměti) může být pro aplikaci nebo pro uživatele podstatně komplikovanější. Další vysvětlení jsou v částech "Zpracování výjimek od knihovních funkcí", "Zpracování výjimek od ovládacích prvků" a "Definice vlastních výjimek".
Deklarací chráněného bloku definujeme konkrétní reakci na výjimky, které mohou vzniknout v průběhu zpracování tohoto bloku. Vznikne-li při zpracování bloku výjimka, přeruší se provádění chráněného bloku, zpracuje se definovaná reakce a potom dojde k opuštění bloku.
Uveďme příklad programu s chráněným blokem. Vznikne-li při zpracování bloku výjimka, začne se zpracovávat část pro zpracování výjimky a zazní zvukový signál. Provádění programu pokračuje za koncem bloku.
try {začátek chráněného bloku} a:=0; {vznikne-li výjimka v některém příkazu} b:=100/a; writeln('Tohle nikdo neuvidí'); except {... skočí se sem} on Exception do MessageBeep(0); {při každé chybě se ozve zvukový signál} {a pak se pokračuje za koncem chráněného} {bloku }
V nejjednodušším případě lze například chránit přidělení systémových prostředků a uvnitř chráněného bloku definovat blok, který přiděluje a chrání další systémové prostředky. Uveďme zde základní schéma této definice:
{přidělení prvního zdroje } try {přidělení druhého zdroje } try {kód, který používá oba zdroje } finally {uvolní druhý zdroj } end; finally {uvolní první zdroj } end;
Vnořené bloky lze použít i k lokálnímu zpracování konkrétních výjimek, které předefinovává zpracování definované ve vnějším bloku. Základní schéma této definice vypadá následovně:
try {chráněný kód } try {speciálně chráněný kód } except {lokální zpracování výjimky } end; except {globální zpracování výjimky } end;
Lze také kombinovat bloky pro různé druhy reakcí na výjimky, vnořovat ochranu systémových prostředků do bloků pro konkrétní výjimky a naopak.
Je třeba si uvědomit, že výjimky nevznikají v kódu programu. Nastavit výjimku může například knihovní procedura nebo nějaký ovládací prvek. Programový kód musí zajistit, aby byly systémové prostředky uvolněny i v tomto případě.
Mezi běžné systémové prostředky, které je třeba vždy uvolnit, patří soubory, paměť, systémové prostředky Windows a objekty.
Následující zpracování události například přiděluje paměť a potom generuje chybu, takže kód pro uvolnění paměti se neprovede:
procedure TForm1.Button1Click(Sender:TComponent); var APointer: pointer; AnInteger, ADividend: integer; begin ADividend:=0; GetMem(APointer, 1024); {přidělí 1K paměti} AnInteger:=10 div ADividend; {dělí nulou} FreeMem(APointer,1024); {nikdy se neprovede} end;
Ačkoliv většina chyb není takhle evidentních, příklad ilustruje jeden podstatný moment: Když nastane chyba při dělení nulou, dojde k ukončení bloku, takže se příkazu FreeMem nikdy nepodaří paměť uvolnit.
Abychom zajistili, že procedura FreeMem uvolní paměť přidělenou procedurou GetMem, je třeba umístit programový kód do chráněného bloku.
{přidělení prostředku} try {příkazy používající prostředek} finally {uvolnění prostředku} end;
Klíčem k použití bloku try...finally je skutečnost, že aplikace vždy provede příkazy v části bloku začínající klíčovým slovem finally, i když při provádění chráněného bloku vznikne chyba. Kdykoliv programový kód v části try (nebo kterákoliv procedura volaná z této části) vytvoří výjimku, provádění příkazů pokračuje částí finally, kterou nazýváme ukončující kód. Pokud k výjimce nedojde, ukončující kód je normálně proveden za všemi příkazy v části try. Zde je příklad zpracování události, která přidělí paměť a generuje chybu, přesto však přidělenou paměť uvolní:
procedure TForm1.Button1Click(Sender:TComponent); var APointer:pointer; AnInteger, ADividend: integer; begin ADividend:=0; GetMem(APointer, 1024); {přidělí 1K paměti} try AnInteger := 10 div ADividend; {dělí nulou} finally FreeMem(Apointer, 1024); {provede se po chybě} end; end;
Příkazy v ukončujícím kódu nezávisí na typu výjimky ani na tom, zda výjimka nastala či nikoliv.
Poznámka:
Blok pro ochranu systémových prostředků neobsluhuje výjimku.
Ukončující kód ani nemá informaci o tom, zda vůbec výjimka nastala,
a nemůže proto rozhodnout, zda a jak má výjimku obsloužit. Dojde-li
k výjimce uvnitř bloku pro ochranu systémového prostředku, provede se
ukončující kód a pak ukončí blok, přičemž výjimka zůstane nahozena.
Výjimku může případně obsloužit nadřazený blok.
Existují rovněž němé výjimky, které implicitně žádnou zprávu nezobrazují. Princip němých výjimek je obsloužen samostatně.
Knihovní funkce vytváří těchto sedm typů výjimek:
Modul SysUtils definuje obecnou V/V výjimku typu EInOutError, která obsahuje atribut se jménem ErrorPre indikující výskyt chyby. Tento atribut je dostupný v instanci objektu výjimky postupem uvedeným v části "Práce s instancemi výjimek".
Výjimka | Význam |
EOutOfMemory | V hromadě není volný prostor pro přidělení požadované paměti. |
EInvalidPointer | Aplikace se pokouší vrátit přidělenou paměť, ukazatel však směřuje mimo hromadu. Obvykle to znamená, že vrácení paměti již bylo provedeno. |
Následující tabulka uvádí výjimky pro celočíselnou aritmetiku odvozené od EIntError.
Výjimka | Význam |
EDivByZero | Pokus o dělení nulou |
ERangeError | Hodnota nebo výraz mimo povolený rozsah |
EIntOverflow | Přetečení při celočíselné operaci |
Následující tabulka uvádí výjimky pro aritmetiku v pohyblivé řádová čárce odvozené od EMathError.
Výjimka | Význam |
EInvalidOp | Procesoru byla předložena neznámá instrukce |
EZeroDivide | Pokus o dělení nulou |
EOverflow | Přetečení v pohyblivé řádové čárce |
EUnderflow | Podtečení v pohyblivé řádové čárce |
Modul SysUtil definuje obecnou hardwarovou výjimku typu EProcessorException. Knihovní funkce nevytvářejí instance výjimky EProcessorException, ale vytvářejí výjimky z této třídy odvozené.
Hardwarové výjimky odvozené od EProcessorException jsou uvedeny v následující tabulce.
Výjimka | Význam |
EFault | Základní objekt, od něhož jsou odvozeny následující tři výjimky |
EGPFault | Obecné porušení ochrany paměti, typicky způsobené použitím neinicializovaného ukazatele nebo objektu |
EStackFault | Chybný přístup do zásobníkového segmentu |
EPageFault | Chyba při použití souboru výměny |
EInvalidOpPre | Zpracování neznámé instrukce. Typicky vzniká, když se procesor pokouší provádět data nebo neinicializovanou paměť. |
EBreakPoint | Ladicí přerušení nastavované v prostředí Delphi. |
ESingleStep | Ladicí přerušení při provedení jednoho kroku programu. |
S hardwarovými výjimkami snad s výjimkou porušení ochrany paměti byste se měli potkávat pouze zřídka, neboť představují závažné chyby v prostředí operačního systému. Výjimky pro bod přerušení a jeden krok programu jsou používány uvnitř integrovaného prostředí Delphi.
Při definici obsluhy výjimky uzavřeme programový kód, kterým chceme výjimku obsloužit, do části except bloku pro zpracování výjimky. Typický blok pro obsluhu výjimky vypadá následovně:
try {chráněné příkazy} except {obsluha výjimky} end;
Aplikace provádí příkazy uvedené v části except pouze tehdy, když se vyskytne výjimka v průběhu zpracování části try. Provedení příkazů v části {\bf try} zahrnuje i provedení procedur volaných z části try. To znamená, že pokud je z části try volána procedura nebo funkce, která nedefinuje svojí obsluhu výjimky, provádění programu se vrací do bloku pro obsluhu výjimky.
Pokud některý příkaz v části try vytvoří výjimku, provádění programu se přesune na začátek části except a prochází se zde uvedené příkazy pro obsluhu výjimky až po nalezení té obsluhy, která se vztahuje k právě nastalé výjimce.
Jakmile je nalezena odpovídající obsluha výjimky, příkazy v ní uvedené se provedou a objekt výjimky je automaticky zrušen. Výpočet pokračuje za koncem chráněného bloku.
on < typ výjimky > do < příkaz >;
Můžeme například definovat obsluhu pro dělení nulou, která dosadí implicitní hodnotu:
function SpoctiPrumer(Soucet, Pocet:integer):integer; begin try Vysledek:=Soucet div Pocet; except on EDivByZero do Vysledek:=0; end; end;
Toto řešení je čistší než testování jmenovatele před každým voláním funkce. Ekvivalentní funkce zapsaná bez použití výjimky vypadá následovně:
function SpoctiPrumer(Soucet, Pocet:integer):integer; begin if Pocet <> 0 then Vysledek := Soucet div} Pocet else Vysledek := 0; end;
Rozdíl mezi těmito dvěma funkcemi je v podstatě rozdíl mezi programováním s výjimkami a bez použití výjimek. Uvedený příklad je zcela jednoduchý, ale snadno si lze představit komplikovanější výpočty zahrnující stovky kroků, z nichž každý může selhat, pokud je jeden nebo více vstupů chybných.
Při použití výjimek máme možnost uvést "normální" tvar algoritmu a potom obsloužit výjimečné případy, pro který tento tvar neplatí. Bez použití výjimek je třeba vždy opakovaně testovat, zda je možno pokračovat v provádění následujícího kroku.
Pokud je třeba v obsluze výjimky získat i informaci vztaženou k instanci výjimky, použijeme speciální tvar fráze on..do, který umožňuje přístup k instanci výjimky. Tento tvar vyžaduje použití pomocné proměnné, do níž se uloží odkaz na instanci.
Vytvořme například nový projekt s jediným formulářem obsahujícím rolovací lištu a tlačítko. Obsluha tlačítka bude obsahovat jediný příkaz:
ScrollBar1.Max := ScrollBar1.Min - 1;
Tento příkaz vede k chybě, protože maximální pozice rolovací lišty musí být vždy větší než minimální pozice. Implicitní obsluha výjimky zobrazí dialog obsahující zprávu o výskytu výjimky. Tuto obsluhu lze předefinovat a vytvořit vlastní dialog používající zprávu o výjimce:
try ScrollBar1 := ScrollBar1 - 1; except on E:EInvalidOperation do MessageDlg('Ignoruji výjimku: ' + E.Message, mtInformation, [mbOK], 0); end;
Typ pomocné proměnné (v tomto příkladu E) je uveden za
dvojtečkou (v tomto příkladu EInvalidOperation). K přetypování
výjimky na požadovaný typ lze případně použít operátor as.
Poznámka:
Instanci výjimky nikdy nerušíme. Zrušení instance je
zajištěno automatickým zpracováním výjimky. Pokud zrušíme instanci
sami, aplikace se jí pokusí zrušit znovu a způsobí tak havárii
aplikace.
Pokud není pro danou výjimku uvedena v bloku specifická obsluha, provádění opouští blok a vrací se do nadřazeného bloku (nebo do části programu, odkud byl blok vyvolán), přičemž výjimka zůstává nahozena. Tento postup se opakuje až po návrat do bloku, v němž je výjimka obsloužena, případně až po návrat na úroveň aplikace (tj. prakticky k ukončení reakce programu na poslední akci uživatele).
try {příkazy} except on E_neco do {obsluha specifické výjimky}; else {implicitní obsluha výjimek};
Vytvoření implicitní obsluhy výjimky pro daný blok zajistí, že blok nějakým způsobem zpracuje každou výjimku a nedojde tak k přechodu do nadřazeného bloku.
POZOR! Tuto vše zahrnující implicitní obsluhu výjimky bychom měli použít jen velmi opatrně. Fráze else zajišťuj obsluhu všech výjimek, včetně těch, o nichž nevíme nic. Obecně by měly být zpracovány pouze výjimky, které umíme obsloužit. V ostatních případech je lepší provést ukončující kód a ponechat zpracování výjimky té části programu, která má více informací o výjimce a o její obsluze.
Následující příklad naznačuje řešení, které zpracovává všechny celočíselné matematické výjimky:
try {příkazy provádějící celočíselné matematické operace} except on EIntError do {specifická obsluha pro chyby v celočíselné aritmetice} end;
I tak lze zpracovat konkrétní výjimku specifickým způsobem, obsluhu konkrétní je však třeba uvést před obsluhu obecnou, protože se obsluhy prohledávají v tom pořadí, v jakém byly uvedeny.
Následující blok například zpracovává specificky chyby rozsahu a obecně všechny chyby v celočíselné aritmetice:
try {příkazy provádějící celočíselné matematické operace} except on ERangeError do {zpracování chyby rozsahu}; on EIntError do {zpracování ostatních chyb v celočíselné aritmetice}; end;
Pokud by byla obsluha pro EIntError uvedena před obsluhou pro ERangeError, specifická obsluha pro ERangeError by nebyla provedena nikdy.
Můžeme například chtít při výskytu výjimky zobrazit uživateli nějakou zprávu a potom pokračovat standardní obsluhou výjimky. Deklarujeme tedy lokální obsluhu výjimky, která zobrazí zprávu a potom provede příkaz raise. Toto tzv. znovuvytvoření výjimky ukazuje následující příklad:
try {příkazy} try {speciální příkazy} except on E_neco do begin {obsluha pro speciální příkazy} raise; {znovuvytvoření výjimky} end; end; except} on E_neco do ...; {obecné zpracování} end;
Pokud programový kód v části {příkazy} vytvoří výjimku E_neco, je provedena pouze obsluha z vnější fráze except. Pokud však výjimku E_neco vytvoří programový kód v části {speciální příkazy}, je provedena obsluha z vnitřní fráze except následovaná obecnější obsluhou z vnější fráze except.
Opakovaným vytvořením výjimky lze snadno přidat speciální obsluhu výjimky pro speciální případy bez ztráty existující obsluhy nebo nutnosti ji duplikovat.
Běžným zdrojem chyb v ovládacích prvcích je chyba rozsahu intervalu u vlastností obsahujících index. Pokud seznam například obsahuje tři položky (0..2) a aplikace se pokouší zpracovat položku s číslem 3, seznam vytváří výjimku "Index mimo povolený rozsah".
Následující příklad obsahuje obsluhu výjimky, která upozorňuje uživatele na chybný index položky seznamu:
procedure TForm1.Button1Click(Sender:TObject); begin ListBox1.Items.Add('první položka'); ListBox1.Items.Add('druhá položka'); ListBox1.Items.Add('třetí položka'); try Caption := ListBox1.Items[3]; {nastavuje titulek formuláře podle čtvrté položky seznamu} except on EListError do MessageDlg('Seznam obsahuje méně položek než čtyři', mtWarning, [mbOK],0); end; end;
Při prvním odeslání tlačítka má seznam jenom tři položky, tudíž přístup ke čtvrté položce (Items[3]) vede k chybě. Druhé odeslání tlačítka přidává k seznamu další řetězce a k výjimce tedy již nedojde.
Němé výjimky se uplatní v případě, kdy nechceme výjimku obsloužit, ale chceme zrušit prováděnou operaci. Zrušení operace má blízko k opuštění bloku pomocí procedur Break a Exit, tímto způsobem je však možno opustit i několik vnořených bloků.
Všechny němé výjimky jsou odvozeny od standardní výjimky EAbort. Standardní implicitní obsluha výjimek zobrazuje dialog se zprávou pro všechny výjimky kromě výjimek odvozených od typu EAbort.
Vytvoření němé výjimky je možno provést i zkráceným způsobem, voláním procedury Abort. Procedura Abort automaticky vytváří výjimku typu EAbort, která ukončí prováděnou operaci bez toho, že by zobrazovala chybovou zprávu.
Následující příklad obsahuje jednoduché ukončení operace. Ve formuláři obsahujícím prázdný seznam a tlačítko doplníme do události OnClick tohoto tlačítka následující programový kód:
procedure TForm1.Button1Click(Sender:TObject); var i:integer; begin for i:=1 to 10 do begin ListBox1.Items.Add(IntToStr(i)); {zařazuj do seznamu čísla} if i=7 then Abort; {po sedmé položce skonči} end; end;
Standardní obsluha výjimek zpracovává výjimky odvozené od třídy Exception. Proto je vhodné odvozovat nové třídy výjimek od této třídy nebo od některé ze standardních odvozených tříd. Pokud je potom v některém bloku vytvořena výjimka nově deklarovaného typu a není pro ní definována specifická obsluha, bude obsloužena standardním způsobem.
Vezměme například následující deklaraci:
type EMojeVyjimka = class(Exception);
Standardní obsluha pro typ Exception je provedena i v případě, že je vytvořena EMojeVyjimka, ale není pro ní definována specifická obsluha. Protože standardní obsluha pro typ Exception zobrazí jméno vytvořené výjimky, lze minimálně ověřit, že k vytvoření této výjimky skutečně došlo.
Při vytváření výjimky lze rovněž specifikovat hodnotu pro uložení do proměnné ErrorAddr. Příkaz raise doplníme o klíčové slovo at následované adresovým výrazem. Při deklaraci tvaru
type EChybneHeslo = class(Exception);
lze například vytvořit výjimku "chybné heslo" uvedením příkazu raise s uvedením instance výjimky EChybneHeslo podobně jako v následujícím příkladu:
if Heslo <> SpravneHeslo then raise EChybneHeslo.Create('Zadáno chybné heslo');
Pomocí obsluhy výjimek v aplikaci lze zajistit, že nedojde ke ztrátě systémových prostředků při nenadálém ukončením aplikace, případně umožní aplikaci nebo uživateli chybový stav opravit a zkusit operaci znovu. Minimálně poskytují výjimky aplikaci potřebné informace a mechanismus nutný k obsloužení chybových stavů.