fel
le

Kivételkezelés

A programozás során függvényekből építkezünk, függvényeket hívunk. A függvények (metódusok) futása vagy rendben lezajlik, elvégzik a feladatukat, kiszámolják amit ki kell, vagy valami oknál fogva ezt nem tudják megtenni. Ilyen esetre számtalan példát lehet mondani:

  • egy file megnyitó függvény nem tudja megnyitni a file-t, mert az nem létezik
  • egy hálózati portot megnyitó függvény nem tudja megnyitni a portot, mert az már foglalt, vagy a windows tűzfal azt megtagadta neki
  • egy vektort átlagát kiszámító függvény nem tudja az átlagot meghatározni, mert a vektor 0 elemet tartalmaz

Hogyan jelezzen vissza egy ilyen jellegű függvény az őt meghívó helyre, hogy problémába ütközött?

  • az semmiképpen nem helyes megoldás, hogy hibaüzenetet ír ki a képernyőre. Ugyanis a hibaüzenetet legfeljebb a felhasználó tudja elolvasni, nem a függvényt meghívó kód. A felhasználó vagy megérti a hibaüzenetet, és tud vele kezdeni valamit, vagy nem. Állandó probléma a hibaüzenet nyelve (magyarul?, angolul?), nyelvezete (ha túlságosan szakmai, akkor sok felhasználó meg sem érti), stb. Ennél nagyobb baj az, hogy ezen függvény nem jól hordozható, hiszen ha ugyanezen függvényt egy web-es felületű programban kívánjuk használni, akkor ott a kiírása meg sem jelenik a kliens gépen.
  • hasonló okoknál fogva nem helyes hangjelzést adni hiba esetén
  • hibakóddal térni vissza már használhatóbb ötlet, de ezzel is sok a probléma. A hibakód általában egy egész számérték, de nincs szabványosítva a jelentése. Mit jelenthet az, ha a hálózati portos függvény -1-el tért vissza, és mi lehet a baj, ha -5-el? Minden egyes függvénynek saját értelmezése van a hibakódokra, így a programozóknak folyton a súgót kell olvasnia ilyen esetekben.

Sokkal nagyobb a baj, hogy a hibakódos verzió sem használható minden esetben. Például egy property set részének belsejében semmiképpen, hiszen az nem tér vissza semmilyen értékkel:

class Akarmi
{
    private int _x;
    public int x
    {
      set
      {
        if (value<100) _x=x;
        else return -1; // ?? ez nem megy itt
      }
    }
}
 

Hasonlóképpen nem megy a hibakódos megoldás konstruktortok belsejében sem:

class Akarmi
{
    public Akarmi(int x)
    {
        if (x<100) _x=x;
        else return -1; // ?? ez itt sem megy
      }
    }
}
 

Eleve nem használható a hibakódos visszatérés olyan függvények esetén, akik már amúgy is térnek vissza értékkel. Pl. a fenti vektor elemeinek átlagát meghatározó függvény milyen hibakóddal térjen vissza probléma esetén, amelyről a hívás helyén el lehet dönteni, hogy a kiszámolt átlag, hanem hibakód?

További problémája a hibakódos megoldásnak, hogy a hívó kód elfelejtheti ellenőrízni a függvény visszatérési értékét, és abban a hiszemben futhat tovább, hogy minden rendben van. Persze ennek előbb-utóbb valami súlyos következménye lesz, esetleg a program egy későbbi ponton már olyan helyzetbe kerül, hogy leáll futási hibával. De addigra messze kerül a végrehajtás a láncreakciószerűen terjedő hibától, és nehéz azonosítani a probléma kiinduló okát.

Kivételek készítése

Az OOP stílusban a hibajelzés és kezelés fenti problémájára is megoldást kerestek. Szabványos megoldást, vagyis minden OOP programozó és függvény ezzel a módszerrel jelzi a saját hibás működését, annak okát. A hívó kód kénytelen ellenőrízni a hiba jelentkezését, különben nem futhat tovább.

A technológia neve kivétel kezelés, vagy idegen nyelvi szakszóval exception handling. A kivétel (exception) maga a hiba jelzése. Ez gyakorlatilag egy objektumpéldány, mely leírja a hiba keletkezésének körülményeit megfelelő részletességgel. A példány mezői hordozzák a szükséges információkat, akár olyan részletességgel, hogy melyik függvény melyik sorát nem sikerült végrehajtani, és miért nem sikerült. Persze ilyen részletességgel ritkán írjuk le a hibát, inkább csak a hiba okát szoktuk megjelölni, pontos helyét nem.

Több ilyen objektumosztály áll rendelkezésre, amelyből hibaleíró példányt készíthetünk, az egyik legegyszerűbb ilyen az Exception osztály:

Exception hiba = new Exception("File nem megnyitható");
hiba.HelpLink = "http://www.sajatprogram.hu/support/faq";
 

Jellemző, hogy a konstruktorban megadunk egy hibaüzenetet, mely leírja a hiba tényleges okát. Jegyezzük meg azonban, hogy ez a szöveg nem azért készült, hogy a hívó kód feldolgozza. Ez a hibaüzenet a felhasználónak fog megjelenni, de csak abban az esetben, ha ez a hiba olyan súlyosságú, hogy a program leállásához vezet.

A példány elkészítése után finomhangolásokat végezhetünk a hiba leírásán. A fenti példában most épp egy internetes linket adunk meg, ahol a hiba esetleges okáról, és megoldásáról lehet olvasni.

Nem csak az Exception osztályból készíthetünk hibaleíró példányt, hanem tetszőleges, az Exception típussal kompatibilis osztályból is. Ez az OOP-ben gyakorlatilag az Exception gyerekosztályait jelenti.

Azért szoktunk egyébként gyerekosztályt használni, mert egyrészt akkor jobban jellemezhetjük a hiba okát, másrészt a specifikusabb osztály az adott hiba jellegét esetleg kiegészítő mezőkkel képes leírni.

Például ha a hiba tényleges oka valamilyen file nem megnyithatósága, akkor a közönséges 'Exception' helyett annak gyerekosztályát, a 'FileNotFoundException'-t szoktuk alkalmazni. Ennek ugyanis van olyan konstruktora, ahol a hibaüzeneten túl a problémás file nevét is meg lehet adni:

FileNotFoundException hiba = new FileNotFoundException("File nem megnyitható",@"C:\adatok.dat");
 

Rengeteg hibaosztály van eleve beépítve a BCL-be, példaképpen álljon még itt néhány:

  • IndexOutOfRangeException: használjuk például listák vagy tömbök túlindexelése esetén
  • OutOfMemoryException: jellemezheti a túl kevés memóriát
  • DivideByZeroException: 0-val való osztási probléma (nem kiszámítható eredmény)
  • NullReferenceException: valamely változóba (paraméterbe) null érték került, ami nem megengedhető
  • FormatException: a formátum string és a paraméterek nem illeszthetők össze (pl. Console.WriteLine() esetén)

Miután a hibát leíró példányt előkészítettük, a hibát oda kell adni a futtató rendszernek, jelezvén hogy hiba keletkezett. Ez nem automatikus folyamat, hiszen a a fenti kód egy egyszerű példányosítás, melyről a futtató rendszer nem ismeri fel hogy ez hibaleírásra készül. Valamint a futtató rendszer nem tudhatja, mikor fejezzük be a példány finomhangolását, mikor áll készen a hiba leírása. Ezért ennek jelzésére saját utasítás van, a throw (dobás). A szó jelentése miatt gyakran mondjuk: "feldobni a hibát".

throw hiba;
 

Nagyon gyakori, hogy a konstruktorhívásban leírtunk már mindent, nem kívánjuk a hibapéldányt finomítani, ezért a példányosítás, és a feldobás egymást követően helyezkedik el a kódban:

Exception hiba = new Exception("File nem megnyitható");
throw hiba;
 

A fenti kód egyszerű módon összevonható egy sorba:

throw new Exception("File nem megnyitható");
 

Nem általunk okozott kivételek

A BCL osztályai, metódusai is kivétel generálásával jelzik a hibás működést. Az adott metódusra lekérve a súgót (MSDN) elolvashatjuk, hogy milyen kivételekre számíthatunk, milyen esetekben:

1. ábra: A FileStream konstruktor által dobott kivételek

A fenti képen például azt mutatja az MSDN, hogy a FileStream osztály konstruktora milyen esetekben milyen kivételekkel jelzi a hibás működést.

Kivételek mint futási hibák

Amint egy kivétel példány átkerül a futtató rendszerhez a throw segítségével, minden megváltozik. A futtató rendszer tudomásul veszi, hogy a hibát feldobó függvény nem képes a feladatát rendben elvégezni, így további futtatásának nincs értelme. Ezért a throw-t követő utasítások végrehajtása már nem történik meg. Ha a hibát egy ciklus belsejében dobjuk fel, akkor a ciklus is leáll. A végrehajtás gyakorlatilag a függvény végére kerül, és a hibát feldobó függvény visszatér a hívás helyére.

A hívás helyén lehetőségünk van ellenőrízni hogy a meghívott függvény jelzett-e hibát. Amennyiben ezt elmulasztjuk, úgy a futtató rendszer úgy dönt, hogy a meghívó függvény sem képes tovább futni (hiszen futása azon az értéken alapulna, amit a hívott függvénynek kellett volna előállítani). Ezért ezen hívott függvény futása is megszakad, és visszatér az őt meghívó helyre.

Ez a folyamat a fentiek szerint gördül tovább, egészen addig, amíg a visszatérések során visszajutunk egészen a Main() függvényig. Nem áll itt sem le a folyamat, ha a Main() függvényben sem kezeljük le a hibát, akkor a Main() függvényre is ugyanaz a sors vár, mint bármely más függvényre: futása megszakad, és visszatér az őt meghívó helyre. Igen ám, de a Main() mögött már nem áll senki, nincs hova visszatérni. Pontosabban a Main() mögött már csak az operációs rendszer áll, aki a programot betöltötte a memóriába, és elindította a Main() függvényt, s vele együtt a program futását. Amennyiben a Main() függvény futása befejeződik, úgy ez a program leállását jelenti.

Vagyis a throw segítségével jelzett hiba, amennyiben kezeletlenül gördül végig a függvény hívási láncon - a program leállásához vezethet. A futtató rendszer mielőtt végképp hagyná leállni a programot, még annyit megtesz, hogy a hiba leírásába becsomagolt hibaüzenetet egy párbeszédablakba kiírja. Ezzel lehetőséget ad a felhasználónak, hogy tájékozódjon mi történt az előbb még teljes sebességgel futó programban.

Try + Catch blokk

Ha egy programrészletben lévő kód (vagy az ebből a részből hívott függvények bármelyike) "veszélyes", vagyis elképzelhető hogy kivételt generál - akkor ezen kódrész köré hibakezelőt írhatunk. Ezt a try és catch kulcsszavak segítségével lehet megvalósítani (try=próbáld, catch=elkapni):

public int megszamol()
{
    int db;
    try
    {
      FileStream fs = new FileStream(@"C:\adatok.dat", FileMode.Open);
      db = 0;
      ...
      fs.Close();
    }
    catch (FileNotFoundException)
    {
       db = 0;
    }
    return db;
}
 

A fenti függvény például megpróbálja megnyitni a C:\adatok.dat nevű file-t, majd mondjuk megszámolni benne valamely típusú adat darabszámát. Mivel élünk a gyanúperrel, hogy a file akár nem is létezik a célgépen, így a kritikus kódsorokat (FileStream konstruktorhívás, file kezelése) egy try...catch blokkba rakjuk. Amennyiben ezen blokk belsejében FileNotFoundException keletkezne, úgy erre úgy reagálunk, hogy a darabszámot nullára állítjuk, amely ekvivalens azzal, mintha a file létezne, csak éppen nem lenne benn egyetlen adott típusú megszámolnivaló sem.

A fenti függvény elvileg akkor sem okoz futási hibát, programleállást, ha az általa kezelt file nem megnyitható. Ugyanis a saját maga által "okozott" hibát ő maga le is kezeli. A catch elveszi a futtatórendszertől az esetleg oda throw-al átadott hibapéldányt, és a saját hibakezelő blokkjában "reagál" a hibaeseményre. De mivel a hibapéldányt elvették a futtatórendszertől, így a program futása visszaáll a normál üzemmódra.

Amennyiben a try...catch közötti utasítások nem okoznának kivételt, a catch blokk nem hajtódik végre. Amennyiben a try...catch közötti utasítások nem FileNotFoundException-t, hanem valamely más kivételt váltanának ki, akkor a fenti catch blokk nem dolgozná fel a hibát, így a fenti függvény is visszatérne a hívás helyére, megtartva a hibaállapotbeli futást.

Valójában itt is érvényesül a típuskompatibilitás. A fenti catch nem csak a tényleges FileNotFoundException-t kivételeket képes elkapni és feldolgozni, hanem a vele kompatibiliseket is (gyermekosztály-példányok).

Több catch blokk írása

Egy try...catch blokk nem csak egyetlen catch-al zárulhat, hanem akár többel is:

try
{
...
}
catch (FileNotFoundException) { ... }
catch (IndexOutOfRangeException) { ... }
catch (Exception) { ... }
 

A fenti kódban lévő három catch ág mindegyike más-más típusú hibák esetén lép működésbe. A legutolsó ág, az Exception ág gyakorlatilag minden típusú hibát elkap, hiszen a feldobott hibapéldányok őse kötelezően az Exception osztály, így minden feldobott hiba kompatibilis ezzel az osztállyal.

Amennyiben ilyen több-catch-es feldolgozást írunk, mindeképpen ügyeljünk az ágak megfelelő sorrendjére:

try
{
...
}
catch (IOException) { ... }
catch (FileNotFoundException) { ... }
catch (Exception) { ... }
 

A fenti esetben az a baj, hogy a FileNotFoundException az IOException osztály gyerekosztálya. Így ha a try utáni kódban ilyen jellegű kivétel képződne, akkor az IOException ág már leszedné ezt a hibát, és feldolgozná. Így a rákövetkező 'FileNotFoundException' ágnak már nincs szerepe. Ezt egyébként a fordítóprogram jelzi is: 'A previous catch clause already catches all exceptions of this or of a super type (System.IO.IOException)'. Vagyis: az előző catch szakasz már elkapja az összes ilyen típusú hibát.

Paraméter nélküli catch

Amikor catch ágat írunk, általában megadjuk milyen típusú kivételek kezelését végzi el ez az ág. Nyilván a catch (Exception) jellegű feldolgozó ág minden felbukkanó kivételtípusra reagál.

Ez valójában nem teljesen így volt a .NET 1.0 -ban. Ugyanis a C#-ban nem lehetséges a throw segítségével olyan kivétel-példányt átadni a futtatórendszernek, amely nem az Exception osztályból, vagy valamely gyerekosztályából származik - de más programnyelveken igen (pl. C++). Amennyiben C#-ban egy ilyen másik fajta nyelven megírt DLL valamely függvényével dolgoztunk, akkor fel kellett készülnünk akár arra is, hogy az érkező kivétel-példány nem Exception-kompatibilis. Ezeket a kivételeket a catch (Exception) ág sem kapta el és dolgozta fel. A sors iróniája, hogy a catch után megjelölt kivétel-osztály is kötelezően vagy maga az Exception, vagy valamely gyerekosztálya kell legyen. Tehát catch (Object) például nem volt írható.

Ezekre az esetekre készült a paraméter nélküli catch.

try
{
...
}
catch (Exception) { ... }
catch {}
 

A paraméter nélküli catch a catch (Object)-et helyettesítette, bár ezt ebben a formában nem lehetett leírni, mivel a C# fordító ezt visszautasította.

A .NET 2.0-ban annyi előrelépés történt ez ügyben, hogy a fenti problémát maga a Framework próbálja megoldani oly módon, hogy a nem Exception típusból készült kivétel-példányokat becsomagolja egy System.Runtime.CompilerServices.RuntimeWrappedException típusú példányba. Ez az osztály már az Exception leszármazottja, így a fenti kód már hibásnak minősül a .NET 2.0-ban, ugyanis a catch (Exception) már minden típusú kivétellel meg tud bírkózni, mögött már nincs értelme a paraméter nélküli catch-nak.

Catch-ben a példány feldolgozása

Amennyiben valamely catch ágban szeretnénk a feldobott példányt meg is vizsgálni, kiolvasni esetleg a specifikus mezőit, ellenőrízni a dinamikus típusát, akkor nem elég a catch után a típusnevet megadni, hanem egy azonosítót is meg kell adni:

try  { ... }
catch (Exception e) { ... }
 

A fenti kódban a catch blokk belsejében most az e azonosítón keresztül tanulmányozhatjuk a futtatónak átadott kivétel-példányt:

try  { ... }
catch (Exception e)
{
if (e.HelpLink!=null && e.HelpLink!=String.Empty)
  MessageBox.Show("Látogass el a "+e.HelpLink+" site-ra, ott bővebben is olvashatsz a hibáról.");
...
}
 

Elképzelhető, hogy megvizsgáljuk az e dinamikus típusát az is operátorral (de nyilván ezt nem ezzel a módszerrel szoktuk megoldani, hanem helyette megfelelő számú és típusú catch ágakat írunk):

try  { ... }
catch (Exception e)
{
    if (e is FileNotFoundException) { ... }
    if (e is IndexOutOfRangeException) { ... }
    ...
}
 

Amennyiben a catch ágban nem hivatkozunk a kivétel-példányra, ne is deklaráljuk azt (csak a típust adjuk meg). Ellenkező esetben (deklaráljuk, de nem használjuk) "The variable e is declared but never used" figyelmeztető üzenetet kapunk.

Saját kivételosztály használata

Bár a BCL rengeteg kivételosztályt tartalmaz már eleve beépítve, lehetőség van saját kivétel-osztályok készítésére is. Ekkor egyszerűen kiválasztjuk a számunkra legmegfelőbb, már létező kivétel-osztályt, és ősként választjuk. Saját kivételosztály készítésénel ez kötelező lépés - saját kivételeinknek is kompatibilisnek kell lenniük az Exception osztállyal!

class myDivideByZeroException:DivideByZeroException
{
public string egyikValtozoNeve;
public string masikValtozoNeve;
public myDivideByZeroException(string uzenet, string a, string b)
 :base(uzenet)
 {
   egyikValtozoNeve = a;
   masikValtozoNeve = b;
 }
}
 

Az ilyen kivétel-osztály jellemzően tartalmaz saját konstruktort, hogy a bővítést, az új hibaleíró mezőket kezelje. Fenti esetben most megadható, hogy mi volt azon két változó neve, amelyek közötti osztási művelet elvezett volna a hibához:

public double atlag(int[] vektor)
{
    int osszeg=0;
    foreach(int x in vektor)
      osszeg+=x;
    if (vektor.Length==0)
      throw new myDivideByZeroException("atlagszamitasi hiba",osszeg,vektor.Length);
    return (double)osszeg / vektor.Length;
}
 

A fenti saját kivételosztály a DivideByZeroException osztály gyermekosztálya. Ennek megfelelően a fenti generált kivételt többféleképpen is elkaphatjuk:

// a saját kivételek speciális elkapása
try { ... }
catch (myDivideByZeroException) { ... }
 

vagy:

// a DivideByZero elkapja a myDivideByZero-t is
try { ... }
catch (DivideByZeroException) { ... }
 

Mi történhet egy catch belsejében?

Amennyiben a függvényük olyan jellegű, hogy képes bizonyos felbukkanó hibák esetén is eredményt produkálni - úgy ezen hibatípusokat érdemes kezelnie is. Amelyekkez azonban nem tud mit kezdeni, azokat át kell engednie magán. Ez jellemzően úgy oldható meg, hogy több catch ágat használunk - így kiszűrjük azokat a hibatípusokat, amelyek esetén a függvény még képes normálisan működni.

Alapértelmezett viselkedésre állás

Egy elkapott kivételt kezelhet a catch belsejében a függvény oly módon, hogy alapértelmezett választ generál:

public bool BelepesiEllenorzes(string loginNev, string jelszo)
{
bool belephet = false;
try
{
   loginNev = loginNev.Trim().ToUpper();
   jelszo = jelszo.Trim().ToUpper();
   foreach(Felhasznalo f in lista)
     if (f.login == loginNev && f.jelszo == jelszo)
     {
       belephet = true;
       break;
     }
}
catch
{
   belephet = false;
}
return belephet;
}
 

A fenti kódban a try...catch közötti szakaszban megpróbáljuk megtalálni, hogy van-e olyan Felhasznalo, akinek egyezik a neve és jelszava a paraméterekben adottakkal. Ebben a kódban sok helyen keletkezhet kivétel. De bármely problémát most úgy tekintünk, hogy a felhasználó nem léphet be. Ezért a catch blokkban beállítjuk az erre az esetre megfelelő választ.

A fenti stílusú kivételkezelés végeredménye, hogy a kivétel eltűnik, megszűnik, a függvény a kivételt feldolgozta.

Kivétel újbóli feldobása

Előfordul, hogy a kivétel elkapása után, alaposabban megvizsgálva a hiba körülményeit úgy döntünk, hogy ezen kivételt mégsem akarjuk kezelni. Ugyanakkor mivel már elkaptuk - elvettük a futtató rendszertől. Hogyan adjuk neki vissza?

try  { ... }
catch (Exception e)
{
   ...
   if (...) throw e;
}
 

Amenyiben a kivételpéldánynak feldolgozáshoz nevet is adunk (e), ennek segítségével az elkapott kivétel-példányt visszadobhatjuk, visszaadhatjuk a futtató rendszernek.

Amennyiben ugyanazt a kivételt kivánjuk visszadobni, ennél egyszerűbben is megoldható. A throw paraméter nélküli változata képes erre:

int fazis = 0;
try  
{
   ...
   fazis=1;
   ...
   fazis=2;
   ...
   fazis=3;
   ...
}
catch (Exception)
{
   if (fazis<3) throw;
   else ...;
}
 

A fenti egyszerű példa azt mutatja, hogy amennyiben a throw belsejében lévő kód nem jutott el a fazis=3 utasításig, akkor a korábbi kódsorok valamelyike okozta a kivételt. Ekkor még a függvényünk nem képes javítani a helyzeten - így a leszedett hibát újra feldobja.

Másik kivétel feldobása

A catch blokkban elvileg lehetőségünk van nemcsak a leszedett kivételt visszadobni, hanem helyettesíteni valamilyen más kivétel-példánnyak (másik kivételt feldobni):

try  
{
   ...
}
catch (Exception)
{
   if (fazis<3) throw new InvalidArgumentException("Nem jo a parameterezes");
   else if (fazis<2) throw new IndexOutOfRangeException("Nem jo vektor");
   else ...;
}
 

Amennyiben egy catch blokk belsejében kivételt dobunk fel (akár újra feldobjuk a már leszedettet, vagy újat készítünk), a program futása újra visszaáll hibaállapotra, az adott catch blokk futása megszakad. Ilyen esetekben feldobott kivételt ugyanazon try további catch ágai nem dolgozhatják fel, csak egy másik try...catch blokk:

try  
{
   ...
}
catch (FileNotFoundException e)
{
   if (e.FileName==null || e.FileName==String.Empty) throw new Exception(e.Message);
   else return false;
}
catch (Exception e) { ... }
 

A fenti példában a FileNotFoundException-t elkapjuk, és bizonyos esetben egy Exception-t dobunk vissza. Azonban ezt az új kivételt a következő catch ág nem dolgozhatja fel, pedig egyébként képes lenne rá.

A try ... catch egymásba ágyazhatósága

A try...catch blokkokat tetszőleges mélységben egymásba ágyazhatjuk:

try
{
...
   try  
   {
      ...
   }
   catch (FileNotFoundException e) { ... }
   catch (NumericException e) { ... }
...
}
catch (Exception) { ... }
 

A fenti kódban az egymásba ágyazás eredménye, hogy a külső catch blokk képes például feldolgozni a belső catch-ek belsejében keletkezett kivételeket is.

A finally

Gyakori eset az alábbi kód-struktúra:

public void Kezeles()
{
 erőforrás-lefoglalás
 try  
 {
    ...
 }
 catch
 {
    erőforrás-felszabadítás
    throw;
 }
 erőforrás-felszabadítás
}
 

A kód lényege, hogy amennyiben a Kezeles függvény futása során kivétel keletkezne, azt változatlan formában adjuk vissza a meghívás helyére, hogy ott is értesüljenek a problémáról. De a Kezeles függvény a munkájához erőforrásokat foglal le (memória, file, nyomtató, ...), melyet mielőtt kilépne - mindenképpen fel szeretne szabadítani szabályos módon.

Az alábbi esetek történhetnek ebben a függvényben:

  • az "erőforrás-lefoglalás" során keletkezik a kivétel. Ez azt jelenti, hogy maga az erőforrás-lefoglalás nem sikerült - így a felszabadítási lépés sem szükséges. A keletkezett kivétel visszatér a hívás helyére, hiszen a foglalás nincs a try belsejében.
  • az "erőforrás-lefoglalás" sikerül, belépünk a try blokkba, és azon belül keletkezik a kivétel. Ekkor mivel maga a lefoglalás sikerült - mindenképpen szeretnénk azt szabályos módon bezárni. Ekkor hajtódik végre a catch blokk, ahol a bezárás befejeződik, és a kivétel-példányt újra feldobjuk (throw;).
  • az "erőforrás-lefoglalás" sikerül, a try blokk belsejében sem keletkezik kivétel. Ekkor a catch blokk nem kerül végrehajtásra, ezért a catch blokk utáni "erőforrás-felszabadítás" fog végrehajtódni.

A catch blokk belsejében lévő throw miatt a catch utáni "erőforrás-felszabadítás" már nem futna le. Ha viszont nem keletkezett kivétel, akkor a catch nem kerül végrehajtásra. Azonban az, hogy ebben a függvényben az "erőforrás-felszabadítás" kétszer kerül lekódolásra - mindenképpen szerencsétlen megoldás.

Erre nyújt megoldást a finally kulcsszó:

public void Kezeles()
{
 erőforrás-lefoglalás
 try  
 {
    ...
 }
 finally
 {
    erőforrás-felszabadítás
 }
}
 

A finally blokk is a try blokkal áll együtt. A finally blokk jellemzője:

  • akkor is végrehajtásra kerül, ha a try blokkban nem keletkezett kivétel
  • akkor is végrehajtásra kerül, ha kivétel keletkezett a try blokkban
  • az esetlegesen keletkezett kivételt a finally nem dolgozza fel, a finally blokk végén az visszaadásra kerül a futtató rendszernek.

try ... catch ... finally

Az alábbi struktúra működik beágyazás nélkül is:

try { ... }
 catch (ExcpTip1) { ... }
 catch (ExcpTip2) { ... }
 finally  { ... }
 

Vagyis a try után egy, vagy több catch blokk következhet, majd egy finally blokk is. A végrehajtás ebben a struktúrában az alábbiak szerint zajlik:

  • a try belsejében ha nem keletkezik kivétel, akkor értelemszerűen a catch-ek nem futnak le, a finally viszont igen.
  • a try belsejében ha kivétel keletkezik, és van olyan catch ág, amely ezt képes feldolgozni, akkor először lefut ezen catch ág, majd a finally
  • ha nincs olyan catch ág, amely a hibát feldogozná, a finally akkor is lefut, de ilyenkor a kivétel visszakerül a finally végén a futtató rendszerhez
  • ha a feldolgozó catch ág is kivételt váltana ki, a finally akkor is lefut, és a kivétel a finally végén visszakerul a futtató rendszerhez

try ... finally ... catch

Ilyen sorrendű kombinált struktúra nincs a C#-ban. Ezt egymásba ágyazott try-okkal kell megoldani:

try
 {
    try { ... }
    finally  { ... }
 }
 catch (ExcpTip1) { ... }
 catch (ExcpTip2) { ... }
 

Kivételek és a többszálú programok

Többszálú programok esetén a program a végrehajtása közben több, külön életet élő szálat hoz létre és indít el. Egy többszálú program akkor fejeződik be, amikor az összes szál leállt. A szálak egymással közös (shared) változókon keresztül kommunikálnak - az egyik szál elhelyez egy értéket egy mezőbe, amelyet a másik szál onnan ki tud olvasni, stb.

A szálakban keletkezett kivételek nem hatnak csak az adott szálon belül. Vagyis az egyik szálon keletkezett kivételt csakis ugyanabban a szálban lehet kezelni. A kezeletlen kivétel csakis az adott szálat állíthatja le, a többi szálat semmiképpen. Vagyis az az állítás, hogy a kezeletlen kivétel mint futás közbeni hiba a program teljes leállásához vezet - csak az egyszálú programokra igaz.

Másrészről amikor egy futó szál egy másik szálat erővel meg akar szakítani, le akar állítani, akkor a szóban forgó szálra meghívja annak Abort() metódusát. Ez gyakorlatilag egy ThreadAbortException-t vált ki a szálban, melyet ha az nem kezel - akkor a szál leállását eredményezi. Kezelni mégis szokták ezt a kivételt a szálak, mivel leállásuk előtt előfordulhat hogy nagytakarítást végeznek. De általában a végén "engedelmeskednek" ezen kivételnek, és leállnak.

Hernyák Zoltán
2013-03-17 19:53:22