A kivételkezelés egy igen hasznos nyelvi tulajdonság, segítségével felkészíthetjük a programot arra, hogy ha futás közbeni hiba történik. Futás közbeni hibának nevezzük az olyan hibákat, amelyekre programírási időben nem lehet, vagy csak nagyon komplikált módon lehetne felkészülni.
Ilyen eset lehet például az, ha lemezre írunk egy fájlt. A lemez a példa kedvéért legyen egy USB meghajtó, ami menet közben eltávolítható. Ha menet közben kihúzza a felhasználó, akkor a programunknak nem fog sikerülni a lemezre írás, ezért lefagy, vagy más szóval elszáll.
Az alábbi példa egy nagyon egyszerű futás idejű hibát generál:
using System;
namespace PeldaFutasidejuhiba
{
class Program
{
static void Main(string[] args)
{
string szoveg = "valami szöveg";
int szam = Convert.ToInt32(szoveg);
Console.WriteLine(szam);
Console.ReadKey();
}
}
}
A programban a hibát az fogja okozni, hogy a szoveg változó értékét nem lehet számmá alakítani, mivel nincs benne számjegy. Ha a szoveg értéke a felhasználótól származik, akkor a programunk potenciálisan sebezhető, összeomlasztható egy véletlenül bevitt szövegrészlettel, ami végső soron nem túl bizalomgerjesztő egy szoftver esetén. Nem véletlen született meg az „all input data is evil” kifejezés. Ez magyarul, kicsit hatásvadász fordításban úgy hangozhatna, hogy „minden beviteli adat az ördögtől származik”.
Az ilyen hibák kivédésére és kezelésre vezették be nyelvi elemként a kivételkezelést. Ezt a funkciót szinte az összes objektum orientált programozást lehetővé tévő nyelv használja.
Ha a programunkat debug üzemmódban indítjuk el, akkor pici gondolkodás után a debugger egy üzenetet dob, miszerint egy kivételt nem kezeltünk. A kivétel típusát és egy rövid leírást is megjelenít róla.
Ha pedig debuggeren kívül indítjuk el a programot, akkor egy „szép” Windows hiba ablakot kapunk, miszerint a program működése leállt.
C# esetén a kivételkezelésre a try-catch blokkpáros alkalmazható. A szintaxis a következő:
try
{
//védett utasítások.
}
catch (kivétel_típusa)
{
//hibakezelés
}
A try blokk tartalmazza a kivételkezeléssel védett utasításokat. Ha a program végrehajtása közben valami hiba lép fel, akkor a try blokkhoz rendelt catch hibakezelőbe akkor kerül a vezérlés, ha a catch feltételben meghatározott típusú hiba lép fel.
Az összes kivételeset, amit kezelhetünk, az Exception osztályból származik, amely rendelkezik egy szöveges leírással és jó néhány nyomkövetési információval, amivel megkönnyíthető a hiba megkeresése. Az előző példában megírt alkalmazásunk kivétel kezelt változata:
using System;
namespace PeldaFutasidejuhiba2
{
class Program
{
static void Main(string[] args)
{
try
{
string szoveg = "valami szöveg";
int szam = Convert.ToInt32(szoveg);
Console.WriteLine(szam);
}
catch (FormatException ex)
{
Console.WriteLine("Valami hiba történt: {0}", ex.Message);
}
Console.ReadKey();
}
}
}
A program kimenete:
Valami hiba történt: Input string was not in a correct format.
Egy catch blokk csak egy típusú hibát kaphat el, de egy try blokk után több catch blokk is jöhet a különböző típusú hibák elkapására és kezelésére. Ha több catch blokk van, akkor a legelső, ami tudja kezelni a kivételt, fog lefutni. Emiatt lényeges, hogy milyen sorrendben vannak definiálva az egyes catch ágak. Jelen esetben a blokk csak FormatException hibákat kap el.
Ez a típusú hiba akkor lép fel, ha szöveget akarunk más típusra konvertálni vagy valamit formázottan szeretnénk szöveggé konvertálni, de a szöveg formátum nem megfelelő.
Írhattam volna a blokkba Exception típust is, mivel az minden jellegű hibát elkap, viszont ennek a túlzásba vitele nem a legjobb programozói gyakorlat.
Ha nagyobb részeket szeretnénk általános Exception elkapással védeni, akkor abba a helyzetbe kerülhet a felhasználó, hogy kap egy hibaüzenetet, de nem tudja konkrétan az okát, hogy miért. Ezért igyekezzünk a kivételkezelő blokkjainkat informatívvá tenni, illetve csak a megfelelő típusú hibák elkapására szorítkozni, ha lehetőségünk van.
Az informatívvá tétel egyik lehetősége, hogy az összes Exception osztályból leszármaztatott hiba rendelkezik Message tulajdonsággal, ami a hiba típusáról ad egy rövid szöveges összefoglalást. Az alábbi felsorolás a .NET fontosabb beépített hiba osztályait tartalmazza és egy rövid leírást arról, hogy mikor következhet be a hiba:
-
AccessViolationException
Olyankor lép fel, ha olyan memória területet szeretnénk írni vagy olvasni, amihez nincs hozzáférésünk.
-
ArgumentException
Akkor futhatunk bele, ha egy metódusnak nem megfelelő paramétereket próbálunk átadni. Leginkább programozói hiba. Ezt kód módosítással kezelni kell.
-
ArgumentNullException
Akkor futhatunk bele, ha egy metódus egyik paraméterének null értéket próbálunk átadni.
-
ArgumentOutOfRangeException
Akkor keletkezik, amikor egy metódus egyik paraméterének átadott érték a lehetséges értékek halmazán kívül esik.
-
ArithmeticException
Általános műveleti hiba. Leginkább konvertáláskor, műveletvégzéskor léphet fel.
-
DivideByZeroException
Nullával való osztáskor fellépő hiba.
-
FormatException
Szöveggé alakításkor és szöveg feldolgozáskor fellépő hiba. Azt jelzi, hogy a szöveges formátum nem megfelelő.
-
IndexOutOfRangeException
Tömb alul vagy felül indexeléskor fellépő hiba.
-
InsufficientMemoryException
Akkor lép fel, amikor az előzetes memória vizsgálat szerint nincs elegendő memória a művelet elvégzéséhez.
-
InvalidCastException
Akkor lép fel, ha nem megfelelően konvertálunk adattípusok között. Például, ha statikusan szeretnénk szövegből int-re konvertálni.
-
NotSupportedException
Akkor lép fel, ha végrehajtani kívánt művelet az objektum jelenlegi állapotában nem támogatott.
-
NullReferenceException
Abban az esetben lép fel, ha a változó, amire a kódrészletünkben hivatkozunk null értékű, de nem kellene neki annak lennie.
-
OutOfMemoryException
A programunk futás közben kifogyott a memóriából.
-
StackOverflowException
Tipikusan akkor fordul elő, ha egy metódusunk rekurzívan saját magát hívja végtelen ciklusban
-
IOException
Fájlkezelési műveletek sikertelensége esetén bekövetkező hiba típus.
Előfordulhat, hogy a felsorolásban szereplő leírások közül nem mindegyik egyértelmű, a könyv későbbi részeiben majd tisztázzuk a fogalmak többségét. A felsorolás igencsak kivonatos egyébként, mivel rengeteg hiba típus létezik amit kezelni tudunk. A https://mikevallotton.wordpress.com/2009/07/08/net-exceptions-all-of-them/ címen található egy részletesebb lista az Exception osztályokról rövid leírással, hogy mikor következhetnek be.
A kódunkból dobhatunk mi is kivételeket. Erre a throw kulcsszó szolgál. A throw kulcsszó után egy Exception leszármaztatott osztály példányosításának kell állnia. A throw kulcsszó önmagában is alkalmazható, de csak egy catch blokkon belül. Ezt leginkább akkor szokták alkalmazni, ha a hibát lokálisan csak naplózással szeretnénk lekezelni.
using System;
namespace PeldaFutasidejuhiba2a
{
class Program
{
static void Main(string[] args)
{
try
{
string szoveg = "valami szöveg";
int szam = Convert.ToInt32(szoveg);
Console.WriteLine(szam);
}
catch (Exception ex)
{
Console.WriteLine("Valami hiba történt: {0}", ex.Message);
throw; //hibát tovább dobjuk.
}
Console.ReadKey();
}
}
}
Jelen esetben a módosított kódunk kiírja a hibaüzenet, majd ugyan úgy összeomlik, mint a kivétel kezelés nélküli változat. Felmerülhet jogosan a kérdés, hogy akkor ennek mi értelme így? Ha kivétel keletkezik és lokális szinten Exception (minden kivétel őse) típusra kezelünk, mert nem vagyunk biztosak abban, hogy milyen kivétel keletkezhet, akkor sosem célszerű elkapni a kivételt, mert sok hiba elnyelődhet, aminek a felderítése igen körülményes tud lenni. Éppen ezért úgy célszerű alkalmazást létrehozni, hogy legyen egy globális hibakezelő try-catch blokk, ami eldöntheti, hogy az adott hiba fennállása mellett futhat-e egyáltalán tovább a program, vagy sem.
A try blokk után állhat egy finally blokk is. A finally blokk opcionális része a kivételkezelésnek. A benne elhelyezett kód akkor is lefut, ha a vezérlés átkerül a catch blokkba. Leginkább olyan esetekben van értelme a használatának, ha kivétel esetén is fel szeretnénk szabadítani az erőforrást, amit használunk.
Erre jó példa lehet fájlkezeléskor, hogy írunk egy fájlt, amibe az írás valamilyen okból kifolyólag meghiúsul. Ekkor a vezérlés átkerül a catch ágba, megjelenítjük a hibát. Viszont a fájl nyitva maradhat, ami ahhoz vezet, hogy más programok nem férhetnek hozzá a tartalmához egészen addig, amíg be nem zárjuk a programunkat.
Az alábbi példa a saját kivétel dobást és a finally blokk használatát mutatja be:
using System;
namespace PeldaKivetelkezeles2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Kettővel szorzó v. 1.0");
Console.WriteLine("Adjon meg egy egész páros számot!");
try
{
var bevitel = Console.ReadLine();
int szam = Convert.ToInt32(bevitel);
if ((szam % 2) != 0)
{
throw new Exception("A szám nem páros");
}
Console.WriteLine($"A szorzás eredménye: {szam * 2}");
}
catch (Exception ex)
{
Console.WriteLine("HIBA történt");
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("Program vége. Nyomjon egy gombot a kilépéshez");
Console.ReadKey();
}
}
}
}
A program egy lehetséges kimenete:
Kettővel szorzó v. 1.0
Adjon meg egy egész páros számot!
1
HIBA történt
A szám nem páros
Program vége. Nyomjon egy gombot a kilépéshez
A példa csak ott sántít, hogy általános Exception osztályt dob a programom. Ennek a legfőbb oka, hogy a könyv még nem tárgyalta az öröklődést és objektumok létrehozását. A hivatalos ajánlás egyébként az lenne, hogy igyekezzünk saját típusú Exception osztályokat dobni saját típusú hibák jelzésére. A könyv későbbi részében erről is lesz szó.
Továbbá bizonyos kivétel típusok dobása nem ajánlott a saját magunk által írt kódból. Ennek az oka az, hogy ha ilyen típusokat dobunk, akkor az összezavarhatja a végfelhasználót vagy más más programozókat, akik a kódunkon dolgoznak. A fontosabb ajánlásokról a https://msdn.microsoft.com/en-us/library/ms229007%28v=vs.110%29.aspx címen érdemes tájékozódni.
Mit érdemes kivételkezelni?
A futás idejű hibák egy része kivédhető már a szoftver megírása közben. Például jelen esetben előre fel lehet készülni arra, hogy ha a felhasználónak lesz lehetősége beírni számnak azt, hogy ASD, akkor meg is fogja tenni. Éppen ezért az ilyen hibákra már az elején érdemes felkészülni és nem kivételkezelő blokkokkal, hanem olyan metódusokkal, amelyek nem dobnak kivételt ilyen esetben.
Erre egy jó példa az int típus TryParse metódusa, ami egy igaz értéket ad vissza, ha a bemenetként adott szöveget sikerült feldolgoznia számként. A feldolgozott számot pedig egy out paraméterben adja vissza:
int szam;
if (int.TryParse("123", out szam))
{
//sikeres volt a feldolgozás
//ebben a blokkban a szam változó
//biztos, hogy helytálló értékkel rendelkezik.
}
A TryParse metódus megtalálható minden alap numerikus .NET típuson, amiről eddig szó volt. Érdemes megjegyezni, hogy használata típusonként egy picit eltérő lehet, a típus jellegzetességéből adódóan.
Ezen felül persze vannak olyan hibák, amire nem tudunk felkészülni előre. Ilyenek a korábban említett I/O és hálózatot érintő témák. Természetesen vannak az olyan hibák is, amiket nem illik lekezelni, mivel sokkal súlyosabb problémát indikálnak.
Ilyen például a NullReferenceException. Ez akkor keletkezik a legtöbb esetben, ha rosszul írtuk meg a programunkat megfelelő körültekintés nélkül. Ezt azért nem kezeljük le try-catch segítségével, mert a hiba tovább gyűrűzhet a szoftverünkben és nem várt mellékhatásokat okozhat.
Ugyan így nem kezeljük le a OutOfMemoryException, IndexOutOfRangeException és StackOverflowException típusokat, mert ezek mind programozási hibákból fakadnak. Ha nagyon betonbiztosra szeretnénk megírni a szoftverünket, akkor csupán egy darab Exception típust kezelő kivételkezelőt teszünk a programunkba utolsó védőbástyaként.
Ez a kivételkezelő lesz azért felelős, hogy a szokásos és semmitmondó Windows hibajelentő helyett a felhasználóink egy értelmes hibaüzenetet kapjanak arról, hogy a program futása közben egy nem várt hiba történt és lépjenek kapcsolatba az ügyfélszolgálattal, vagy jelentsék a hibát valamilyen módon.
Ilyen esetekben célszerű valamiféle naplózást is beépíteni a szoftverbe a hiba felderítése érdekében, de ez már bőven túlmutat a try-catch nyelvi elemen. A naplózásról és a telemetriáról a későbbiek folyamán lesz még szó.