Poikkeukset

Ohjelmointivirheet voidaan karkeasti jakaa kolmeen eri tyyppiin: käännösaikaisiin, ajonaikaisiin ja loogisiin virheisiin. Tässä materiaalissa keskitytään ajonaikaisiin virheisiin, jotka tapahtuvat silloin kun sovellus on suoritustilassa sovelluksen loppukäyttäjällä. Poikkeusten käsittely (Exception Handling antaa ohjelmoijalle mahdollisuuden käsitellä virheet hallitusti ohjelman suorituksen aikana. Ohjelmoijan on tavallaan reagoitava jo ohjelmointikoodissa, mitä sovelluksessa tehdään silloin kun jotain "poikkeuksellista" pääsee tapahtumaan sovelluksessa.
Laadukkaaseen ohjelmointiin kuuluu että ajonaikaiset virheet riippuivatpa ne ohjelman sisäisestä tai ulkoisista asioista käsitellään oikeaoppisesta. Virheet ja poikkeukset eivät saa kaataa ohjelmaa! (Fatal error)

Artikkeleita oikeaoppisesta poikkeusten toteutuksista

MSDN Design Guidelines for Exceptions
Exception Handling Best Practices in .NET

Muutama esimerkki aluksi

Maistellaan muutamia esimerkkejä aluksi, jotta saadaan ideasta kiinni.

Jako nollalla

Kokeillaan aluksi jo peruskoulusta tuttua tilannetta ja jaetaan lukua nollalla.


    static void Main(string[] args)
    {
        int number1 = 100;
        int number2 = 0;
        int result = number1 / number2;
        Console.WriteLine("Result is {0}", result);
    }
    

Ohjelman suoritus pysähtyy riville 6 ja aiheuttaa sovelluksessa DivideByZeroException-tyyppisen poikkeuksen. (kuvakaappaus)

Vääräntyyppinen syöte

Kysytään käyttäjältä lukua ja hän syöttääkin merkkejä.


    static void Main(string[] args)
    {
        Console.Write("Give a number : ");
        string line = Console.ReadLine();
        int number = int.Parse(line);
    }
    

Ohjelman suoritus pysähtyy riville 6 ja aiheuttaa sovelluksessa FormatException-tyyppisen poikkeusen. (kuvakaappaus)

Taulukon virheellinen käyttö

Seuraavassa esimerkissä viitataan taulukon kohtaan, jota ei ole olemassa.


    static void Main(string[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        for (int i=0;i<=5;i++)
        {
             Console.Write("Number is " + numbers[i]);
        }
    }    
    

Ohjelman suoritus pysähtyy riville 7 ja aiheuttaa sovelluksessa IndexOutOfRangeException-tyyppisen poikkeusen. (kuvakaappaus)

Edellä esiteltiin vain muutamia eri poikkeustyyppejä. Eri ohjelmointikielissä on käytössä lukuisia eri poikkeuksia, joita esiintyy/tapahtuu tietyntyyppisissä poikkeuksellisissa tilaiteissa. Käsittelemätön ja tapahtunut poikkeus aiheuttaa ohjelman suorituksen loppumisen (kaatumisen) ja virhe-ilmoituksen näyttämiseen, jos sovellus on ollut ajossa Visual Studiossa. Loppukäyttäjällä sovellus kaatuu ja suoritus keskeytyy.

Poikkeuksien käsittely

C#-ohjelmointikielessä poikkeusten käsittelyyn liittyvä mekanismi auttaa sovelluskehittäjää estämään yllä esitettyjä poikkeuksellisia tilanteita silloin kun sovellus on suoritustilassa. Poikkeusten käsittely hoidetaan try-, catch- ja finally-avainsanoilla. Poikkeukset periytyvät Exception-luokasta, joka on siis kaikkien poikkeuksien kantaluokka.

try

Mahdollisen virhetilanteen aiheuttavat ohjelmointikoodit sijoitetaan try-lohkoon. Virhetilanteen sattuessa ohjelman suoritus jatkuu välittömästi poikkeuksellista tilannetta vastaavassa catch-lohkossa. Kaikki poikkeuksellisen tilanteen aiheuttaneen ohjelmoinnin jälkeiset ohjelmoinnit jätetään suorittamatta try-lohkossa. Jos try-lohkossa ei tapahdu mitään poikkeusta, silloin ei suoriteta mitään catch-lohkoa.

catch

Catch-lohkojen tehtävä on käsitellä try-lohkossa mahdollisesti tapahtuneet poikkeukset. Näitä lohkoja voi olla useita, jolloin jokainen niistä poimii tietyn tyyppiset poikkeukset. Catch-lohkossa voidaan määritellä myös poikkeuksien Exception-kantaluokka, jolloin se nappaa kaikki try-lohkossa tapahtuneet poikkeukset. Tämä menettely lyhentää tietysti ohjelmointikoodin kirjoittamista, mutta tuolloin eri poikkeuksia ei saada eriteltyä.

finally

Try- ja catch-lohkojen jälkeen voidaan määritellä finally-lohko, joka suoritetaan tapahtuipa try-lohkossa poikkeusta tai ei. Finally-lohko toimii yleisesti try- ja catch-lauseiden "viimeistelijä". Siinä vapautetaan/suljetaan mahdollisesti käytössä olleita resursseja (esim. suljetaan tiedostoja, joita on avattu try-lohkossa).


    try {
        // ohjelmoinnit, jotka voivat aiheuttaa poikkeuksen
        // voi olla myös ohjelmointia, joka ei aiheuta poikkeuksia
    } catch (Exception1) {
        // lauseet, jotka suoritetaan jos Exception1-tyyppinen poikkeustapahtuu
    } 
    } catch (Exception2) {
        // lauseet, jotka suoritetaan jos Exception2-tyyppinen poikkeustapahtuu
    } finally {
        // lauseet, jotka suoritetaan tapahtuipa poikkeus try-lohkossa tai ei 
    }
    

Esimerkkien korjaaminen

Korjataan aikaisemmin esitetyt esimerkit käyttämällä poikkeuksien käsittelijöitä.

Jako nollalla


    static void Main(string[] args)
    {
        int number1 = 100;
        int number2 = 0;
        try
        {
            int result = number1 / number2;
            Console.WriteLine("Result is {0}", result);
        } catch (DivideByZeroException)
        {
            Console.WriteLine("Can't divide by zero!");
        }
    }
    

Vääräntyyppinen syöte


    static void Main(string[] args)
    {
        Console.Write("Give a number : ");
        string line = Console.ReadLine();
        try
        {
            int number = int.Parse(line);
            Console.WriteLine("You gave number " + number);
        } catch(FormatException)
        {
            Console.WriteLine("You don't gave a number!");
        }
    }
    

Taulukon virheellinen käyttö


    static void Main(string[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        try
        {
            for (int i = 0; i <= 5; i++)
            {
                Console.WriteLine("Number is " + numbers[i]);
            }
        } catch (IndexOutOfRangeException)
        {
            Console.WriteLine("Wrong index used in array!");
        }
    }    
    

Tiedostoon kirjoittaminen

Aikaisemmissa esimerkeissä ei ollut esillä toimenpiteitä, joita pitäisi joka tapauksessa tehdä try-lohkon jälkeen. Otetaan tässä vaiheessa esille jo tiedostoon kirjoittaminen, vaikka sitä käsitellään materiaalissa toisaalla. Alla olevassa esimerkissä tiedosto avataan try-lohkossa ja suljetaan finally-lohkossa.


	System.IO.StreamWriter outputFile = null;
	try
	{
		outputFile = new System.IO.StreamWriter(@"c:\test.file");
		outputFile.WriteLine("Here is a sample text to file.");
	}
	catch (UnauthorizedAccessException)
	{
		Console.WriteLine("Can't open file for writing (UnauthorizedAccessException)");
	}
	catch (ArgumentNullException)
	{
		Console.WriteLine("Opened stream is null (ArgumentNullException)");
	}
	catch (ArgumentException)
	{
		Console.WriteLine("Opened stream is not writable (ArgumentException)");
	}
	catch (IOException)
	{
		Console.WriteLine("An IO error happend (IOException)");
	}
	catch (Exception)
	{
		Console.WriteLine("Some other exception happend (Exception)");
	}
	finally
	{
		// check for null because OpenWrite might have failed
		if (outputFile != null)
		{
			outputFile.Close();
		}
	}
    

Tiedosto on aina hyvä sulkea finally-lohkossa. Jos try-lohkossa tapahtuu jokin virhe, tällöin Close-metodi voi jäädä suorittamatta (jos tapahtuu poikkeus ennen sitä) ja näin ollen se suljetaan varmasti finally-lauseessa.

Yllä oleva voidaan kirjoittaa lyhemmin käyttämällä poikkeusten Exception-kantaluokkaa. Tällöin ei kuitenkaan saada selville tarkasti mikä poikkeus on tapahtunut.


	System.IO.StreamWriter outputFile = null;
	try
	{
		outputFile = new System.IO.StreamWriter(@"c:\test.file");
		outputFile.WriteLine("Here is a sample text to file.");
	}
	catch (Exception ex)
	{
		//Console.WriteLine("Some exception happened!");
		Console.WriteLine(ex.Message); // Access to the path 'c:\test.file' is denied.
	}
	finally
	{
		// check for null because OpenWrite might have failed
		if (outputFile != null)
		{
			outputFile.Close();
		}
	}
    

Poikkeuksen heittäminen (throw)

Poikkeuksen aiheuttava ohjelmointi voi olla myös muualla ohjelmakokonaisuudessa ja silloin sen tulee heittää mahdollinen tapahtunut poikkeus käsiteltäväksi ylemmälle tasolle kutsuhierarkiassa. Tässä tapauksessa käsitellään ensimmäinen catch-lause, johon poikkeus on sopiva.

Alla olevassa esimerkissä SafeDivision-metodi heittää DivideByZeroException-tyyppisen poikkeusen edelleen käsiteltäväksi, jos metodille parametrina tuotu jakaja (y) on nolla. Tässä tapauksessa tapahtunut poikkeus käsitellään sovelluksen Main-metodissa.


    
    

Omat poikkeusluokat

Oman luokan periyttäminen Exception-luokasta mahdollistaa omien poikkeuksen tekemisen. Tässä tapauksessa oman luokan konstruktorille välitetään parametrina poikkeuksen sisältämä tieto merkkijonona, joka välitetään Exception-luokalle. Poikkeuksen sisältämä merkkijono voidaan esittää loppukäyttäjälle tapahtuneen poikkeuksen olion kautta catch-lohkossa.

Esimerkki: Oma CarException

Alla olevassa esimerkissä toteutetaan Car-luokka ja erillinen CarException-luokka. CarException-luokkaa käytetään apuna heittämään poikkeus, jos autolle ei ole annettu merkkiä tai mallia.

CarException-luokka välittää parametrina saadun merkkijonon Exception-yliluokalle.


    

Car-luokka heittää yllä määritellyn CarException-tyyppisen poikkeuksen, jos autolle ei alusteta mallia tai merkkiä.


    

Pääohjelmassa käsitellään mahdolliset poikkeukset.


    

NotImplementedException

Joskus jonkun metodin toteutus jää vaiheeseen tai sitä ei ole keretty aloittaa, niin on fiksua kertoa asiasta kutsuville ohjelmille. Käytä silloin NotImplementedException -poikkeusluokkaa.

	static bool TestCalc()
    {
	//metodin toteutusta ei ole aloitettu, palautetaan poikkeus 
      throw new NotImplementedException();
    }

Lisätietoa:
Exceptions and Exception Handling (C# Programming Guide)
Using Exceptions (C# Programming Guide)
Exception Handling Best Practices in .NET (Codeproject)