Az Eiffel programozási nyelv

Kivételkezelés

Egy Eiffel rendszer végrehajtása során különböző abnormális események történhetnek. Egy hardware vagy operációs rendszer komponens lehet, hogy nem tudja végrehajtani a feladatát, aritmetikai kifejezés túlcsordulhat, nem jól megírt szoftver elem nem elfogadható eredményt produkálhat. Az ilyen események általában egy jelzést adnak - egy exceptiont, amely megszakítja a végrehajtás normál menetét. Ha a rendszer szövege nem tartalmaz hibakezelést, akkor befejezi a végrehajtást. De persze az a cél, hogy a rendszer szövege tartalmazzon kezelést az exceptionre. Amikor exceptiont használunk, sose felejtsük el, hogy ezek nem normális utasítások, hanem a kivételes, hibás helyzetek lekezelésére szolgálnak.

Eiffel-ben a kivételkezelés alprogram szinten történik. Az alprogram törzse, illetve az utófeltétel után a rescue kulcsszót követően írhatjuk a kivételkezelő részeket. Deferred illetve external típusú alprogramok értelemszerően nem rendelkezhetnek kivételkezelő résszel, mivel deferred alprogram esetében nem lenne értelme kivétekezelést írni, hiszen a törzset nem is definiáltuk, external alprogram esetében pedig a külső programozási eszköz nem ismeri az Eiffel-ben implementált kivételkezelést. (Ld. még alprogramok.)

Az exception következő kategóriái fordulhatnak elő egy rutin végrehajtása közben:

  • (1) Állítás megsértése (amikor állítás-figyelő módban vagyunk).
  • (2) Egy hívott rutin sikertelensége.
  • (3) Egy void értékő entitást hívunk meg egy olyan mővelethez, amihez objektum kell - itt lehet olyan jellemző-hívás, ahol a cél nem üres kell legyen, vagy egy kiterjesztett típusú entitásnak való értékadás, ahol a forrás nem lehet üres.
  • (4) Lehetetlen mővelet - pl. konstruktor, ha nincs elég memória, vagy aritmetikai mővelet, ami túlcsordulást vagy alulcsordulást eredményez.
  • (5) A gép által küldött megszakítási jel - pl. a felhasználó lenyomta a Break gombot, vagy újraméretezi az ablakot, amiben a kurrens processz fut, stb.
  • (6) A szoftver által kiváltott exception - a Kernel Library EXCEPTIONS osztálya által nyújtott lehetőségeken keresztül. Ezek a kategóriák az exception megnyilvánulási módjait különböztetik meg, nem a valódi okát.

    Az exception-ök okai lényegében kétfélék:

  • i. hiba a szoftverben
  • ii. a gép nem képes egy bizonyos mőveletet végrehajtani


    Az előző 1. az i.-be tartozik, vagyis egy korrekt program mindig kielégíti az állításait, a 4. pedig az ii.-be. Bizonyos értelemben a második típusba eső okok az első variánsai, ha a rendszerek nem hajtanának végre egy mőveletet anélkül, hogy előbb ne ellenőriznék őket, akkor egy korrekt rendszer soha nem futna egy exceptionbe. De nem lenne igazán praktikus minden konstruktor utasítás előtt egy check utasítással vizsgálni az elérhető memóriahely mennyiségét, vagy minden összeadás előtt check utasítással vizsgálni, hogy az eredmény beleesik-e a gép által ábrázolt határok közé. Az ilyen esetekben az a priori check-ek drágák, és valószínőleg csak egy kis százalékuk nem eredményes. Ezek azok az esetek, amikor az exceptionre szükség van. Egy abnormális helyzet felismerése, és esetleges helyereállítása - miután bekövetkezett.

    Exception kezelési politika

    Mi történhet egy exception után? Mit tehetünk, amikor a váratlan bekövetkezett? Az exception egy olyan esemény bekövetkezése, ami nem engedi, hogy egy komponens teljesítse vállalt kötelezettségét. Nem elfogadható reakció lenne úgy terminálni a komponenssel, hogy csendben visszatér, és a felhasználó azt hiszi, hogy minden rendben. Mivel a dolgok nem mentek normálisan - a szerződés nem lett teljesítve, így egy ilyen politika majdnem elkerülhetetlenül bajt csinálna.

    Az Eiffel nyelv fejlesztői három csoportra osztották az esetleg előforduló kivételeket:

  • Téves riasztás: ez az eset olyankor áll elő, ha tulajdonképpen nem is történik valódi hiba, csupán esetleg a környezet állapota változik meg, pl. ablakátméretezés történik. Ilyenkor az objektum zavartalanul folytathatja tovább a mőködését, néhány komponens újrarajzolása után.
  • Újrakezdés: ebben az esetben a program fejlesztője előre számít egy kivétel előfordulására, így ezt belekalkulálja a rendszer mőködésébe, felajánlva egy alternatív megoldást és az újrakezdés lehetőségét.

  • Szervezett pánik: ez az az eset, amikor már semmilyen módon nem lehet megmenteni az objektum eddigi állapotát. Ilyenkor az a követelmény, hogy a kivételkezelő résznek helyre kell állítania az osztály invariánsát. (ld. az előző példa)

    Az utolsó két esetre nyelvi elem nyújt megoldást, az elsőre pedig - mivel az a legritkább - a Kernel Library kivételkezelést támogató osztálya, az EXCEPTIONS osztály. (Ld. még szabványos könyvtárak.) Mivel ezek a mechanizmusok eljárás-szinten vannak definiálva, az alacsonyabb szintő komponensekre - mint pl. egy utasítás vagy egy hívás - nincs nyelvi mechanizmus. Ez azt jelenti, hogy egy ilyen komponens végrehajtására tett sikertelen kisérlet (pl. egy konstruktor, amikor nincs elég memória, vagy egy void objektumra meghívott függvény) esetén csak a szervezett pánik politika lehetséges, vagyis a komponens végrehajtása sikertelen lesz.

    Egy szoftver komponens végrehajtása egy bizonyos rutin hívását jelenti, amit a kurrens rutinnak nevezünk. Amikor a komponens végrehajtása nem sikerül, ez egy exception kiváltását eredményezi a kurrens rutinban, ami így az exception átvevője (recipient) lesz. A továbbiakban tehát minden egy-egy rutinra vonatkozik.

    Szervezett pánik

    A rescue záradék segítségével lehet megadni egy rutin válaszát olyan exception-ekre, amelyek a rutin végrehajtás során fellépnek. Ez a rutin deklaráció egy opcionális része, amit a rescue kulcsszó vezet be.

    Példa:

    attempt_transaction(arg : CONTEXT) is -- Try transaction with arg; -- if impossible, reset current object require ... do ... ensure ... rescue reset(arg) end -- attempt_transaction


    Ami a rutin törzs végrehajtása közben fellép (do.. záradék), mint exception (tetszőleges exception!) a rescue záradék végrehajtását fogja eredményezni. Itt ezt a záradékot a reset eljárás hívása alkotja, ami azt jelenti, hogy visszaviszi az objektumot egy stabil állapotba, ami kielégíti az osztályinvariánst. A rescue záradék terminálása egyben a rutin végrehajtás terminálását is jelenti - és ekkor (ha nincs retry utasítása rescue záradék végén) a rutin végrehajtása elszáll.

    Más szavakkal ez a szervezett pánik politikáját illusztrálja - stabil állapotba viszi az objektumot, és terminál, értesítvén a hívót, hogy sikertelen volt. (Egy új exception-t fog kiváltani a hívóban.) Ahogy említettük, a szervezett pánik ki kell elégítse az invariánst. Ezen követelmény formális verziója az, hogy a rescue záradék minden olyan ága, ami nem retry-val végződik, egy olyan állapotot kell eredményezzen, ami kielégíti az invariánst, függetlenül attól, hogy milyen állapotban lépett fel. (pl. lehet egy konstruktort hívni, ami az objektumot olyan állapotba viszi, hogy kielégíti az invariánst.

    Default rescue

    Eiffel-ben az ANY osztályban (minden felhasználói osztály ősosztályában) definiálva van egy default_rescue nevő metódus, melynek törzse alapértelmezésben csak egy üres utasítás. Ez az a feature, ami minden olyan esetben implicit módon meghívódik, amikor explicite nem adunk meg egy alprogram végén rescue részt, és az alprogram mőködése során mégis kivétel váltódik ki. Mivel minden felhasználói osztály az ANY class leszármazottja, ezért minden osztály felüldefiniálhatja ezt a metódust, saját igényei szerint. További öröklés esetén a felüldefiniált feature a leszármazottakra is érvényes lesz. Mivel Eiffel-ben a konstruktorok feladata az osztály-invariáns beállítása, ezért a default_rescue-t gyakran a paraméter nélküli konstruktor szinonímájaként szokták definiálni, hogy szervezett pánik esetén biztosítsa az invariáns visszaállítását. (Ld. még helyességbizonyítás.)

    Példa:

    class C creation make, ... Other creation procedures if any ... inherit ANY redefine default_rescue end feature make, default_rescue is -- No precondition do -- Appropriate implementation; -- Must ensure the invariant end -- make, default_rescue ... Other features ... end -- class C



    Újrakezdés

    Eiffel-ben lehetőség van kivétel kiváltódása esetén az alprogram törzsének újbóli végrehajtására. Természetesen ennek csak akkor van értelme, ha valamilyen módon biztosítjuk, hogy másodszorra a törzsnek egy másik, alternatív része hajtódjon végre. Az újrakezdést úgy tudjuk biztosítani, hogy a rescue záradék végén feltüntetjük a retry kulcsszót.

    Példa:

    try_once_or_twice is -- Solve problem using method 1 or, if unsuccessful, method 2 local already_tried : BOOLEAN do if not already_tried then method_1 else method_2 end rescue if not already_tried then already_tried := true; retry end end -- try_once_or_twice


    A fenti példa kihasználja azt, hogy Eiffel-ben a lokális változók létrejöttükkor kezdeti értéket is kapnak: a logikai típusú változók hamis kezdőértékkel keletkeznek. (Ld. még változók kezdeti értéke.)

    Ha a példában a második kísérlet sem sikerül, akkor a kivételkezelő rész már nem tudja helyreállítani a helyes állapotot, így a szervezett pánik esete áll elő: mindenképpen olyan kivétel váltódik ki, ami továbbterjed az alprogramhívási láncban.

    Olyan megoldást is kereshetünk a fenti problémára, ami nem vált ki kivételt, csupán jelzi a hívónak - pl. egy impossible nevő publikus feature-ben -, hogy a végrehajtás sikertelen volt.

    Példa:

    try_and record is -- Attempt to solve problem using method 1 or, -- if unsuccessful, method 2. Set impossible to true -- if neither method succeeded, false otherwise local already_tried : BOOLEAN do if not already_tried then method_1 elseif not impossible then method_2 end rescue if already_tried then impossible := true end; already_tried := true; retry end -- try_and_record


    A példában bemutatott modell könnyedén bővíthető több kísérlet esetére is: egy számláló változót bevezetve és azt a rescue záradékban mindig megnövelve, amíg el nem ér egy maximális értéket. Retry csak rescue záradékban fordulhat elő. Például a rescue által hívott rutin nem tartalmazhat retry-t, így a default_rescue átdefiniálása sem. Más szóval a default_rescue használata nem vezethet újrakezdéshez. Ennek oka az egyszerőség és az olvashatóság támogatása, a rescue záradékon kívül egy retry nem elég informatív - mit is próbáljunk újra?

    Rendszer failure és az exception history tábla

    Szervezett pánik esetén egy kivétel lép fel a hívóban, ha sikertelen volt a végrehajtás. De mi történik, ha nincs hívó? Ez csak akkor fordulhat elő, ha ez egy original call volt, a gyökérbeli létrehozó eljárás során. Ez ugyanis általában úgy mőködik, hogy hív más rutinokat, és azok ismét másokat. Az original call egy sikertelensége a rendszer failure-hez vezet. A rendszer végrehajtása befejeződik, miközben egy megfelelő diagnosztikát ad arról, hogy a rendszer nem tudja végrehajtani a feladatát. A hiba általában nem a legfelsőbb szinten szokott bekövetkezni, hanem valahol mélyen a hívásláncban. Ha újrakezdéssel nem lett lekezelve, akkor felér a gyökérbeli induláshoz.

    Például, ha egyik rutinnak sincs rescue záradéka, egy osztály sem definiálja át a default_rescue-t, akkor minden exception továbbadódik, és az eredmény a rendszerhiba.

    Mi történik ekkor? A rendszernek van egy eszköze (ami nem a nyelv része), az ún. history tábla, ez egy diagnosztikát ad a hibás hívási láncról.

    Az exception kezelés szemantikája

    Az exception kezelési szemantika definíciójához jó tudni, hogy minden rutinnak van egy explicit vagy implicit rescue záradéka. Egy C osztály egy tetszőleges r rutinjának van egy rb rescue blokkja, ami egy Compound (összetett utasítás), a következőképpen definiálva:

  • Ha r-nek van egy rescue záradéka, akkor rb az ezen záradékban található Compound.
  • Ha nincs, akkor rb a C-beli default_rescue-ra vonatkozó hívásból álló egyetlen utasítás.

    Egy r rutin végrehajtása során fellépő exception, ha se nem ignoráltuk, se nem folytatódott, a következő eseménysorozathoz vezet:

  • A hátralévő utasítások nem kerülnek végrehajtásra.
  • A rutin rescue blokkja kerül végrehajtásra.
  • Ha a rescue blokk egy retry-t hajt végre, a rutin törzse újra végrehajtódik, ez a kurrens exception processzt befejezi. Egy exception bármely új kiváltása egy új exception, amit megfelelően le kell kezelni.
  • Ha a rescue blokk végrehajtása retry nélkül ért véget, ez a kurrens exception processzt befejezi és az r kurrens végrehajtását is, ezen végrehajtás egy failure-t eredményez. Ha van hívó rutin, akkor ez az exception itt egy kivételt vált ki, ami rekurzív módon le lesz kezelve az itt adott szemantikának megfelelően. Ha nincs hívó rutin, akkor r a gyökérbeli create eljárás, a végrehajtása terminálni fog.

    A definíció azt mondja, hogy ezt egy olyan rutinra kell alkalmazni, ami se ignorálva nem lett, sem folytatva. Ez a Kernel Library-beli EXCEPTIONS osztály feature-eire vonatkozik, amit a hamis riasztás eseteinél alkalmazhatunk:

  • Specifikálhatjuk, hogy egy bizonyos típusú exception-t ignoráljon általában.
  • Specifikálhatjuk, hogy egy bizonyos típusú exception egy kijelölt eljárás végrehajtását, és azután folytatást eredményezzen.

    A harmadik lépésben a retry az r törzsét csak újra végrehajtja, de nem ismétli meg az argumentum átadást és a lokális entity inicializálást! Ez teszi lehetővé, hogy egy új próbálkozásnál más ösvényt válasszunk.

    Exception helyesség

    A rescue záradék szerepe, hogy váratlan eseményekkel is bánni tudjunk. Bár egy jól megtervezett rendszerben ezeket csak ritkán fogjuk végrehajtani, bizonyos speciális feltételek vannak, hiszen az objektumok konzisztenciáját fenn kell tartani. Egy rutin failure az aktuális objektumot (a legutolsó hívás célobjektumát) egy konzisztens állapotban kell hagyja, ami kielégíti az invariánst, hogy ne gátolja meg, hogy esetleg egy másik rutin, amelyik használja és képes pl. újrakezdésre, egy jó eredményt adjon. Hasonlóan, egy retry utasítás után is, ami újraindítja a rutin törzsét, a rutin előfeltételének igaznak kell lenni.

    Ezek vezetnek az exception helyesség fogalmához, ez egy olyan feltétel, ami az osztály helyességhez kell. (Egy osztály helyes, ha konzisztens (minden rutin, ami egy előfeltételnek megfelelő állapotban indul, az utófeltételnek és az osztály invariánsnak megfelelő állapotban fejeződik be), ciklus-korrekt, check-korrekt és exception-korrekt.)

    Egy C osztály egy r rutinja exception-korrekt akkor és csak akkor, ha a rescue blokkjának minden b ágára a következő igaz:

  • Ha a b egy retry-jal végződik: {true} b {INVC and prer}
  • Ha a b nem retry-jal végződik: {true} b {INVC}

    Az EXCEPTIONS osztály

    Ha az előbbiekben leírt szolgáltatásoknál többet kívánunk igénybe venni a kivételkezelő részekben, akkor a Kernel Library EXCEPTIONS osztályát kell használnunk. Ezt a következőképpen tehetjük meg: azt az osztályt, melyben alkalmazni kívánjuk az EXCEPTIONS osztály elemeit, származtatnunk kell az EXCEPTIONS osztálytól is. (Ld. még szabványos könyvtárak.) Néhány feature az EXCEPTIONS osztályból:

  • assertion_violation : BOOLEAN -- az utolsó kivétel ellenőrzés megsértése volt-e;
  • class_name : STRING -- azon osztály neve, melyben az a kivétel kiváltódott, mely az utolsó kivételhez vezetett;
  • comment : STRING -- az utolsó kivétel szöveges leírása;
  • developer_exception_name : STRING -- az utoljára (raise-zel) kiváltott fejlesztői kivétel neve;
  • exception : INTEGER -- az utoljára kiváltódott kivétel kódja;
  • external_event : BOOLEAN -- az utolsó kivételt külső esemény (OR szignál) okozta-e;
  • raise(ex_name : STRING) -- fejlesztői kivételt kiváltó feature;
  • routine_name : STRING -- azon alprogram neve, melyben az utolsó kivétel eredetét jelentő kivétel kiváltódott;
  • tag_name : STRING -- a megsértett állítás-záradék neve, ha egy állítás megsértéséből származik az exception.

    Az EXCEPTIONS egy integer kódot vezet be minden elképzelhető exception típusra (pl. class_invariant, invalid_inspect_value, precondition, postcondition, loop_invariant, loop_variant, stb.). Az exception jellemző többek között ezek közül veszi fel a kiváltott exception értékét. Ez lehetővé teszi, hogy egy rescue záradékban hivatkozzunk rájuk.

    Téves riasztás

    Mivel a téves riasztás kategóriába eső kivételek viszonylag ritkán fordulnak elő, ezért kezelésükről nem nyelvi elem gondoskodik, mint a másik két esetben, hanem az EXCEPTIONS osztály metódusai. Mivel a fejlesztők feltételezték, hogy téves riasztást csak kívülről érkező esemény (szignál) okozhat, ezért ezek az eszközök csak ilyen típusú kivételek eseténhasználhatók (előfeltételükben szerepel, hogy a paraméterük csak szignál lehet).

  • ignore(except : INTEGER) -- a továbbiakban az adott kódú szignál nem okoz kivételt;
  • catch(except : INTEGER) -- az előző feature ellentéte;
  • continue(except : INTEGER) -- miután meghívtuk ezt az eljárást, minden adott kóddal érkező szignál hatására végrehajtódik az EXCEPTIONS osztályban definiált continue_on_signal metódus, melynek eredetileg üres a törzse, de bármely leszármazottban felüldefiniálható;

    Végezetül érdemes még elmondani, hogy bár az Eiffel kiterjedt kivételkezelő eszközökkel rendelkezik, egy jól megtervezett rendszer nem tartalmaz sok kivételkezelő részt. A program mőködésének lényegét a rutintörzsekben kell elhelyezni, a rescue záradékokban csak olyan eseteket érdemes feldolgozni, melyeket semmilyen más eszközzel nem tudunk kezelni. Ha a rescue záradékok hosszú, bonyolult kifejezéseket tartalmaznak, akkor azt a rendszer nagy valószínőséggel rosszul tervezték meg.

  •