Zpracování výjimek je mechanismus programovacího jazyka určený k popisu reakce programu na běhové chyby a další možné problémy ( výjimky ) , které mohou nastat během provádění programu a vést k nemožnosti (nesmyslnosti) dalšího zpracování programem jeho základní algoritmus. V ruštině se také používá kratší forma termínu: „ vyřizování výjimek “.
Během provádění programu mohou nastat situace, kdy stav externích dat, vstupně-výstupních zařízení nebo počítačového systému jako celku znemožňuje nebo ztrácí smysl další výpočty v souladu se základním algoritmem. Klasické příklady takových situací jsou uvedeny níže.
Výjimečné situace, ke kterým dochází během činnosti programu, lze rozdělit do dvou hlavních typů: synchronní a asynchronní, jejichž principy odezvy se výrazně liší.
Některé typy výjimek lze klasifikovat jako synchronní nebo asynchronní. Například instrukce dělení nulou by měla formálně vyústit v synchronní výjimku, protože k ní logicky dochází přesně při provádění dané instrukce, ale na některých platformách se kvůli hlubokému zřetězení může výjimka ve skutečnosti ukázat jako asynchronní.
Při absenci nativního mechanismu zpracování výjimek pro aplikace je nejběžnější reakcí na jakoukoli výjimku okamžité přerušení provádění a výzva uživateli se zprávou o povaze výjimky. Můžeme říci, že v takových případech se operační systém stává jediným a univerzálním handlerem výjimek. Například tým Dr. _ Watson , který shromažďuje informace o neošetřené výjimce a odesílá je na speciální server společnosti Microsoft .
Je možné výjimku ignorovat a pokračovat, ale tato taktika je nebezpečná, protože později vede k chybným výsledkům programu nebo chybám. Například ignorováním chyby čtení ze souboru datových bloků program obdrží k dispozici nikoli data, která měl číst, ale některá další. Je nemožné předvídat výsledky jejich použití.
Obsluha výjimečných situací samotným programem spočívá v tom, že když nastane mimořádná situace, řízení se přenese na nějaký předdefinovaný handler - blok kódu, proceduru, funkci, která provede potřebné akce.
Pro fungování obslužných rutin výjimek existují dva zásadně odlišné mechanismy.
Existují dvě možnosti připojení obsluhy výjimek k programu: strukturální a nestrukturální zpracování výjimek.
Nestrukturální zpracování výjimek je implementováno jako mechanismus pro registraci funkcí nebo obslužných rutin příkazů pro každý možný typ výjimky. Programovací jazyk nebo jeho systémové knihovny poskytují programátorovi alespoň dva standardní postupy: registraci handleru a zrušení registrace handleru. Volání prvního „svazuje“ handler na konkrétní výjimku, zavolání druhého toto „vázání“ rozvazuje. Pokud dojde k výjimce, provádění hlavního programového kódu je okamžitě přerušeno a začíná provádění handleru. Po dokončení handleru se řízení přenese buď do nějakého předem určeného bodu v programu, nebo zpět do bodu, kde došlo k výjimce (v závislosti na zadané metodě zpracování - s návratem nebo bez něj). Bez ohledu na to, která část programu právě běží, na konkrétní výjimku vždy zareaguje poslední handler pro ni zaregistrovaný. V některých jazycích zůstává registrovaný handler platný pouze v rámci aktuálního bloku kódu (postupy, funkce), poté není procedura odregistrování vyžadována. Níže je podmíněný fragment kódu programu s nestrukturálním zpracováním výjimek:
SetHandler(ErrorDB, Goto ErrorDB) // Je nastaven handler pro výjimku "DB Error" - příkaz "GoTo Error DB" ... // Zde jsou operátory pro práci s databází JumpTo ClearErrorDB // Příkaz nepodmíněného skoku - obejití obslužné rutiny výjimek ErrorDB: // label - zde proběhne přechod v případě chyby databáze dle nainstalovaného handleru ... // obslužná rutina výjimek DB RemoveOshDB: // popisek - zde dojde ke skoku, pokud je řízený kód spuštěn bez chyby databáze. ClearHandler (chyba DB) // Obslužný program byl odstraněnNestrukturální zpracování je prakticky jedinou možností pro zpracování asynchronních výjimek, ale u synchronních výjimek je to nepohodlné: často musíte volat příkazy pro instalaci / odstranění handlerů, vždy existuje nebezpečí narušení logiky programu přeskočením registrace nebo odhlášením psovoda.
Zpracování strukturálních výjimek vyžaduje povinnou podporu programovacího jazyka – přítomnost speciálních syntaktických konstrukcí. Takový konstrukt obsahuje blok řízeného kódu a obsluhu(y) výjimek. Nejobecnější forma takové konstrukce (podmíněná):
StartBlock ... // Kontrolovaný kód ... if (podmínka) then CreateException Exception2 ... Výjimka manipulátoru 1 ... // Kód obslužné rutiny pro Exception1 Výjimka manipulátoru 2 ... // Kód obsluhy pro Exception2 HandlerRaw ... // Kód pro zpracování dříve neošetřených výjimek EndBlockZde jsou „StartBlock“ a „EndBlock“ klíčová slova , která vymezují blok řízeného kódu, a „Handler“ je začátek bloku pro zpracování odpovídající výjimky. Pokud dojde k výjimce uvnitř bloku, od začátku k prvnímu handleru, pak dojde k přechodu na handler pro něj napsaný, po kterém se celý blok ukončí a provádění bude pokračovat příkazem, který následuje za ním. Některé jazyky nemají speciální klíčová slova pro omezení bloku řízeného kódu, místo toho lze do některých nebo všech syntaktických konstrukcí, které kombinují více příkazů, zabudovat obsluhu výjimek. Takže například v jazyce Ada může jakýkoli složený příkaz (začátek - konec) obsahovat obsluhu výjimky.
"RawHandler" je obslužná rutina výjimky, která neodpovídá žádné z výše popsaných v tomto bloku. Obslužné rutiny výjimek lze ve skutečnosti popsat různými způsoby (jeden handler pro všechny výjimky, jeden handler pro každý typ výjimky), ale v principu fungují stejně: když dojde k výjimce, první odpovídající handler se nachází v tomto bloku, je proveden jeho kód, po kterém skončí prováděcí blok. Výjimky mohou nastat jak v důsledku programových chyb, tak i jejich explicitním generováním pomocí příslušného příkazu (v příkladu příkazu „CreateException“). Z pohledu psovodů se takto uměle vytvořené výjimky nijak neliší od ostatních.
Bloky zpracování výjimek se mohou opakovaně vnořovat do sebe, buď explicitně (textově) nebo implicitně (například procedura je volána v bloku, který sám má blok zpracování výjimek). Pokud žádný z handlerů v aktuálním bloku nemůže zpracovat výjimku, pak provádění tohoto bloku okamžitě skončí a řízení se přenese na další vhodný handler na vyšší úrovni hierarchie. Toto pokračuje, dokud není nalezena obslužná rutina a nezpracuje výjimku, nebo dokud výjimka neopustí obslužné programy určené programátorem a není předána výchozí systémové obsluze, která zhroutí program.
Někdy je nepohodlné dokončit zpracování výjimky v aktuálním bloku, to znamená, že je žádoucí, aby když dojde k výjimce v aktuálním bloku, handler provedl nějaké akce, ale výjimka byla nadále zpracovávána na vyšší úrovni ( obvykle se to stane, když handler tohoto bloku nezpracuje výjimku úplně, ale pouze částečně). V takových případech je vygenerována nová výjimka v obslužné rutině výjimky nebo obnovena pomocí speciálního příkazu, který se dříve vyskytl. Kód obslužné rutiny není v tomto bloku chráněn, takže v něm vyvolaná výjimka bude zpracována v blocích vyšší úrovně.
Kromě řízených bloků kódu pro zpracování výjimek mohou programovací jazyky podporovat bloky zaručeného dokončení. Jejich použití se ukazuje jako výhodné, když je v určitém bloku kódu, bez ohledu na to, zda došlo k nějaké chybě, nutné provést určité akce před jeho dokončením. Nejjednodušší příklad: pokud procedura dynamicky vytváří nějaký lokální objekt v paměti, pak před ukončením této procedury musí být objekt zničen (aby nedocházelo k únikům paměti), bez ohledu na to, zda po jeho vytvoření došlo k chybám či nikoliv. Tato funkce je implementována bloky kódu formuláře:
StartBlock ... // Hlavní kód Dokončení ... // Koncový kód EndBlockPříkazy (hlavní kód) uzavřené mezi klíčovými slovy "StartBlock" a "End" se provádějí postupně. Pokud během jejich provádění nejsou vyvolány žádné výjimky, pak se provedou příkazy mezi klíčovými slovy „End“ a „EndBlock“ (kód ukončení). Pokud se při provádění hlavního kódu vyskytne výjimka (jakákoli), okamžitě se provede ukončovací kód, po kterém je dokončen celý blok a výjimka, která vznikla, nadále existuje a šíří se, dokud není zachycena nějakou výjimkou vyšší úrovně. manipulační blok.
Zásadní rozdíl mezi blokem s garantovaným dokončením a zpracováním je v tom, že nezpracovává výjimku, ale pouze garantuje provedení určitého souboru operací před aktivací mechanismu zpracování. Je snadné vidět, že blok se zaručeným dokončením je snadno implementován pomocí obvyklého strukturovaného mechanismu zpracování (k tomu stačí zadat příkaz k vyvolání výjimky bezprostředně před dokončením řízeného bloku a správně napsat kód handleru) , ale přítomnost samostatné konstrukce vám umožňuje učinit kód transparentnějším a chrání před náhodnými chybami.
Většina moderních programovacích jazyků jako Ada , C++ , D , Delphi , Objective-C , Java , JavaScript , Eiffel , OCaml , Ruby , Python , Common Lisp , SML , PHP , všechny jazyky platformy .NET atd. mají nativní podporu zpracování strukturálních výjimek . Když v těchto jazycích dojde k výjimce podporované jazykem, zásobník volání se rozvine na první obslužnou rutinu výjimky odpovídajícího typu a řízení se přenese na obsluhu.
Kromě drobných rozdílů v syntaxi existuje pouze několik možností zpracování výjimek. U nejběžnějších z nich je výjimka generována speciálním operátorem ( thrownebo raise) a samotnou výjimkou je z pohledu programu nějaký datový objekt . To znamená, že generování výjimky se skládá ze dvou fází: vytvoření objektu výjimky a vyvolání výjimky s tímto objektem jako parametrem . Konstrukce takového objektu přitom sama o sobě nezpůsobuje vyvolání výjimky. V některých jazycích může být objektem výjimky objekt libovolného datového typu (včetně řetězce, čísla, ukazatele atd.), v jiných pouze předdefinovaný typ výjimky (nejčastěji má název Exception) a popř. , jeho odvozené typy (typy -children, pokud jazyk podporuje objektové schopnosti).
Rozsah obslužných rutin začíná speciálním klíčovým slovem trynebo pouze značkou začátku bloku (například begin) a končí před popisem obslužných rutin ( catch, except, resque). Může existovat více obslužných programů, jeden po druhém, a každý může určit typ výjimky, kterou zpracovává. Zpravidla se neprovádí výběr nejvhodnějšího handleru a provede se první handler, který je typově kompatibilní s výjimkou. Proto je důležité pořadí handlerů: pokud se handler, který je kompatibilní s mnoha nebo všemi typy výjimek, objeví v textu před konkrétními handlery pro konkrétní typy, pak konkrétní handlery nebudou použity vůbec.
Některé jazyky také umožňují speciální blok ( else), který se provede, pokud nebyla vyvolána žádná výjimka v odpovídajícím rozsahu. Běžnější je možnost garantovaného dokončení bloku kódu ( finally, ensure). Pozoruhodnou výjimkou je C++, kde takový konstrukt neexistuje. Místo toho se používá automatické volání destruktorů objektů . Zároveň existují nestandardní rozšíření C++, která také podporují funkčnost finally(například v MFC ).
Obecně může zpracování výjimek vypadat takto (v nějakém abstraktním jazyce):
zkuste { řádek = konzole . readLine (); if ( line . length () == 0 ) throw new EmptyLineException ( "Řádek načtený z konzole je prázdný!" ); konzole . printLine ( "Ahoj %s!" % line ); } catch ( výjimka EmptyLineException ) { console . printLine ( "Ahoj!" ); } catch ( výjimka výjimky ) { konzola . printLine ( "Chyba: " + výjimka . zpráva ()); } else { konzole . printLine ( "Program běžel bez výjimky" ); } konečně { konzole . printLine ( "Program se ukončí" ); }Některé jazyky mohou mít pouze jeden handler, který sám zpracovává různé typy výjimek.
Výhody používání výjimek jsou patrné zejména při vývoji knihoven procedur a softwarových komponent orientovaných na masové využití. V takových případech často vývojář přesně neví, jak má být s výjimkou naloženo (při psaní univerzální procedury pro čtení ze souboru nelze předem předvídat reakci na chybu, protože tato reakce závisí na programu, který používá proceduru), ale toto nepotřebuje - stačí vyvolat výjimku A, jejíž handler je poskytnut k implementaci uživatelem komponenty nebo procedury. Jedinou alternativou k výjimkám v takových případech je vracet chybové kódy, které jsou nuceny být předávány řetězem mezi několika úrovněmi programu, dokud se nedostanou na místo zpracování, čímž se kód zanese a sníží se jeho srozumitelnost. Použití výjimek pro kontrolu chyb zlepšuje čitelnost kódu oddělením zpracování chyb od samotného algoritmu a usnadňuje programování a používání komponent třetích stran. A zpracování chyb může být centralizováno v .
Bohužel implementace mechanismu zpracování výjimek je vysoce závislá na jazyce a dokonce i kompilátory stejného jazyka na stejné platformě mohou mít značné rozdíly. To neumožňuje transparentní předávání výjimek mezi částmi programu napsanými v různých jazycích; například knihovny, které podporují výjimky, jsou obecně nevhodné pro použití v programech v jiných jazycích, než pro které jsou navrženy, a ještě více v jazycích, které nepodporují mechanismus zpracování výjimek. Tento stav výrazně omezuje možnost použití výjimek např. v UNIXu a jeho klonech a pod Windows, protože většina systémového softwaru a nízkoúrovňových knihoven těchto systémů je napsána v jazyce C, který výjimky nepodporuje. V souladu s tím, aby bylo možné pracovat s API takových systémů využívajících výjimky, je třeba napsat knihovny obalů, jejichž funkce by analyzovaly návratové kódy funkcí API a v případě potřeby by generovaly výjimky.
Podpora výjimek komplikuje jazyk a kompilátor. Snižuje také rychlost programu, protože náklady na zpracování výjimky jsou obvykle vyšší než náklady na zpracování chybového kódu. Proto se na místech v programu, která jsou kritická pro rychlost, nedoporučuje vyvolávat a zpracovávat výjimky, i když je třeba poznamenat, že v programování aplikací jsou případy, kdy je rozdíl v rychlosti zpracování výjimek a návratových kódů skutečně významný, velmi vzácné. .
Správná implementace výjimek může být v jazycích s automatickým voláním destruktoru obtížná . Když se v bloku vyskytne výjimka, je nutné automaticky volat destruktory objektů vytvořených v tomto bloku, ale pouze těch, které ještě nebyly běžným způsobem smazány. Požadavek na přerušení aktuální operace, když dojde k výjimce, je navíc v rozporu s požadavkem na povinné automatické mazání v jazycích s autodestruktory: pokud dojde k výjimce v destruktoru, pak bude buď kompilátor nucen odstranit neúplně uvolněný objekt. nebo objekt zůstane existující, to znamená, že dojde k nevracení paměti . Výsledkem je, že generování nezachycených výjimek v destruktorech je v některých případech prostě zakázáno.
Joel Spolsky věří, že kód navržený pro zpracování výjimek ztrácí svou linearitu a předvídatelnost. Pokud jsou v klasickém kódu výstupy z bloku, procedury nebo funkce nalezeny pouze tam, kde je programátor výslovně označil, pak v kódu s výjimkami může nastat výjimka (potenciálně) v libovolném příkazu a není možné přesně zjistit, kde se mohou výjimky vyskytnout. analýzu samotného kódu. V kódu, který je navržen pro výjimky, není možné předvídat, kde blok kódu skončí, a jakýkoli příkaz musí být považován za potenciálně poslední v bloku, v důsledku toho se zvyšuje složitost kódu a snižuje se spolehlivost. [jeden]
Také ve složitých programech jsou velké "hromady" operátorů try ... finallya try ... catch( try ... except), pokud nepoužíváte aspekty.
Zpočátku (například v C ++) neexistovala žádná formální disciplína pro popis, generování a zpracování výjimek: jakákoliv výjimka může být vyvolána kdekoli v programu, a pokud pro ni v zásobníku volání neexistuje žádný handler, vykoná se program abnormálně přerušeno. Pokud funkce (zejména funkce knihovny) vyvolá výjimky, pak je program, který ji používá, musí zachytit všechny, aby byla stabilní. Když se z nějakého důvodu jedna z možných výjimek nevyřeší, program neočekávaně spadne.
Proti takovým vlivům lze bojovat organizačními opatřeními: popisem možných výjimek, které se vyskytují v knihovních modulech, v příslušné dokumentaci. Ale zároveň je vždy šance přeskočit potřebný handler kvůli náhodné chybě nebo nesouladu dokumentace s kódem (což není vůbec neobvyklé). Chcete-li zcela eliminovat ztrátu zpracování výjimek, musíte ke svým obslužným rutinám specificky přidat větev zpracování výjimek „každá druhá“ (která zaručeně zachytí všechny, i dříve neznámé výjimky), ale tato cesta ven není vždy optimální. Skrytí všech možných výjimek může navíc vést k situaci, kdy se skryjí závažné a těžko dohledatelné chyby.
Později řada jazyků, jako je Java, zavedla kontrolované výjimky . Podstatou tohoto mechanismu je přidat do jazyka následující pravidla a omezení:
Externě (v jazyce Java) vypadá implementace tohoto přístupu takto:
int getVarValue ( String varName ) vyvolá SQLException { ... // kód metody, který může obsahovat volání, která by mohla vyvolat SQLException } // Chyba kompilace - nebyla deklarována ani zachycena žádná výjimka int eval1 ( String expression ) { ... int a = prev + getVarValue ( "abc" ); ... } // Správně - výjimka byla deklarována a bude předána dále int eval2 ( String expression ) vyvolá SQLException { ... int a = prev + getVarValue ( "abc" ); ... } // Správně - výjimka je zachycena uvnitř metody a nejde mimo int eval3 ( String expression ) { ... try { int a = prev + getVarValue ( "abc" ); } catch ( SQLException ex ) { // Zpracování výjimky } ... }Zde je metoda getVarValue deklarována tak, aby vyvolala výjimku SQLException. Každá metoda, která ji používá, proto musí výjimku buď zachytit, nebo ji deklarovat jako vyvolanou. V tomto příkladu způsobí metoda eval1 chybu kompilace, protože volá metodu getVarValue, ale nezachytí ani nedeklaruje výjimku. Metoda eval2 deklaruje výjimku a metoda eval3 ji zachytí a zpracuje, přičemž obě jsou správné pro řešení výjimky vyvolané metodou getVarValue.
Kontrolované výjimky snižují počet situací, kdy výjimka, kterou bylo možné zpracovat, způsobila kritickou chybu v programu, protože kompilátor sleduje přítomnost obslužných rutin . To je užitečné zejména pro změny kódu, kdy to začne dělat metoda, která dříve nemohla vyvolat výjimku typu X; kompilátor bude automaticky sledovat všechny případy jeho použití a kontrolovat vhodné handlery.
Další užitečnou vlastností kontrolovaných výjimek je, že přispívají ke smysluplnému zápisu obslužných rutin: programátor jasně vidí úplný a správný seznam výjimek, které se mohou na daném místě v programu vyskytnout, a místo toho může pro každou z nich napsat smysluplnou obsluhu. vytvoření „pro jistotu » Společný obslužný program pro všechny výjimky, který reaguje stejně na všechny abnormální situace.
Zaškrtnuté výjimky mají i nevýhody.
Kvůli těmto nedostatkům se tento mechanismus často obchází při použití kontrolovaných výjimek. Mnoho knihoven například deklaruje všechny metody jako vyvolávající nějakou obecnou třídu výjimek (například Exception) a handlery jsou vytvořeny pouze pro tento typ výjimky. Výsledkem je, že vás kompilátor nutí psát obslužné rutiny výjimek i tam, kde nejsou objektivně potřeba, a bez přečtení zdrojů je nemožné určit, které podtřídy deklarovaných výjimek metoda vyvolá, aby na ně byly zavěšeny různé obslužné rutiny. jim. Správnějším přístupem je zachytit nové výjimky uvnitř metody, generované volaným kódem, a v případě potřeby výjimku předat dále – „zabalit“ ji do výjimky již vrácené metodou. Pokud se například metoda změní tak, že začne přistupovat k databázi místo k systému souborů, může sama chytit SQLExceptiona místo toho vyvolat nově vytvořenou IOException, přičemž jako důvod uvede původní výjimku. Obvykle se doporučuje zpočátku deklarovat přesně ty výjimky, které bude muset volající kód zvládnout. Řekněme, že pokud metoda načítá vstupní data, pak je vhodné, aby deklarovala IOException, a pokud pracuje s SQL dotazy, pak by bez ohledu na povahu chyby měla deklarovat SQLException. V každém případě je třeba pečlivě zvážit sadu výjimek vyvolaných metodou. V případě potřeby má smysl vytvořit si vlastní třídy výjimek odvozené z výjimek nebo jiných vhodných zaškrtnutých výjimek.
Všeobecně nelze provést kontrolu všech výjimek, protože některé výjimečné situace jsou ze své podstaty takové, že jejich výskyt je možný na kterémkoli nebo téměř libovolném místě v programu a programátor jim nedokáže zabránit. Zároveň je zbytečné uvádět takové výjimky v popisu funkce, protože by to muselo být provedeno pro každou funkci bez zpřehlednění programu. V zásadě se jedná o výjimky související s jedním ze dvou typů:
Je nelogické a nepohodlné odstraňovat takové chyby ze systému zpracování výjimek, už jen proto, že někdy jsou stále zachyceny a zpracovávány. Proto jsou v systémech s kontrolovanými výjimkami některé typy výjimek odstraněny z kontrolního mechanismu a fungují tradičním způsobem. V Javě se jedná o třídy výjimek zděděné z java.lang.Error – fatální chyby a java.lang.RuntimeException – chyby za běhu, obvykle související s chybami v kódování nebo nedostatečnými kontrolami v kódu programu (špatný argument, přístup pomocí nulové reference, výstup mimo hranice pole , nesprávný stav monitoru atd.).
Hranice mezi „opravitelnou“ a „fatální“ chybou je velmi libovolná. Například chyba I/O v desktopovém programu je obvykle „opravitelná“ a je možné o ní uživatele informovat a pokračovat v běhu programu. Ve webovém skriptu je to také „fatální“ - pokud se to stalo, stalo se něco špatného s prováděcím prostředím a je třeba zastavit zobrazením zprávy.
Typy dat | |
---|---|
Neinterpretovatelné | |
Numerický | |
Text | |
Odkaz | |
Kompozitní | |
abstraktní | |
jiný | |
související témata |