Exceptions in C#
Inleiding
Voor een computerprogramma is het al snel einde verhaal als het te maken krijgt met een onvoorziene (exceptionele) omstandigheid. Voorzien in al het onvoorziene? Dat kan niemand tenzij je paranormaal begaafd bent. Gelukkig kunnen we wel het onvoorziene afvangen met een exception zodat we op zijn minst een wat vriendelijkere foutmelding kunnen geven indien een onvoorziene omstandigheid zich voordoet.
Niks afvangen
De situatie kan zich voordoen dat tijdens een berekening gedeeld wordt door nul. “Delen door nul is flauwekul” en laat ons eens kijken wat er gebeurt als we delen door nul en we verder geen voorzieningen treffen:
using System;
namespace Exceptioneel
{
class MainClass
{
public static void Main(string[] args)
{
int teller = 10;
int noemer = 0;
int uitkomst = teller / noemer
}
}
}
Het programma stopt ermee en we krijgen deze (onvriendelijke) foutmelding:
Unhandled Exception:
System.DivideByZeroException:
Attempted to divide by zero.
at Exceptioneel.MainClass.Main
(System.String[] args) [0x00006]
in /.../Program.cs:10
[ERROR] FATAL UNHANDLED EXCEPTION:
System.DivideByZeroException:
Attempted to divide by zero.
at Exceptioneel.MainClass.Main
(System.String[] args)
[0x00006] in /.../Program.cs:10
Press any key to continue...
Try Catch
Een gebruiker ervaart de bovenstaande melding als vaag, bedreigend en onvriendelijk. Het mag dus eigenlijk wel wat gebruikersvriendelijker en we gaan daarvoor gebruik maken van exceptions waarbij de exception wordt opgevangen door een exception handler:
using System;
namespace Exceptioneel
{
class MainClass
{
public static void Main(string[] args)
{
try
{
int teller = 10;
int noemer = 0;
int uitkomst = teller / noemer;
}
catch(Exception ex)
{
Console.WriteLine(
"Er is iets fout gegaan.");
Console.WriteLine("Foutmelding: " +
ex.Message);
Console.WriteLine(
"Controleer wat u heeft ingevoerd.");
}
}
}
}
De exception handler vangt de exception op (catch) zodra de exception zich voordoet (throw) en onderstaande boodschap wordt nu getoond. Het programma stopt er nog steeds mee, maar de toon waarop het programma ermee stopt, dat is nu toch wat gemoedelijker.
Er is iets fout gegaan.
Foutmelding: Attempted to divide by zero.
Controleer wat u heeft ingevoerd.
Press any key to continue...
De exception handler beperkt zich in bovenstaand geval tot het tonen van een wat gebruikersvriendelijkere foutboodschap, maar een exception handler kan meer. Het zou bijvoorbeeld open gebleven verbindingen met databases kunnen sluiten of andere vormen van “opruimwerk” kunnen doen.
Stacktrace
We hebben gezien hoe we met een exception iets afvangen. Een gebruikersvriendelijke foutboodschap zal in de meeste gevallen volstaan voor de gebruiker en met meer wil je hem/haar ook niet lastig vallen.
Voor een programmeur / developer ligt de zaak anders. Hij/ zij zal toch wat meer informatie moeten hebben om het probleem opgelost te krijgen en die info kan verkregen worden middels de stacktrace.
Laat ons dat eens toelichten aan de hand van dit hoofdprogramma:
using System;
namespace Exceptioneel2
{
class MainClass
{
public static void Main(string[] args)
{
try
{
// input
int aantalVruchten = 45;
string vrucht = "kersen";
// verwerking
int aantalDoosjes =
VruchtenPerDoosje(vrucht, aantalVruchten);
// output
Console.WriteLine(
"We hebben voor {0} {1} {2} doosjes nodig.",
aantalVruchten, vrucht, aantalDoosjes);
}
catch(Exception ex){
Console.WriteLine("Iets gaat fout...");
Console.WriteLine(ex.Message + "\r\n");
Console.WriteLine(ex.StackTrace);
}
}
Het hoofdprogramma roept een functie VruchtenPerDoosje aan en functie VruchtenPerDoosje bepaalt aan de hand van de vrucht hoeveel vruchten er in een doosje mogen zitten. We laten bewust een “bug” los in functie VruchtenPerDoosje. De bug is dat voor kersen het aantal vruchten per doos altijd nul is en dat gaat fout in functie functie AantalDoosjes.
static int VruchtenPerDoosje(
string vrucht,
int aantalVruchten)
{
try
{
// vruchten per doos
int aantalVruchtenPerDoosje;
if (vrucht == "kersen")
// bij kersen, 15 stuks per doos
aantalVruchtenPerDoosje = 0; // --> de bug..
else
// anders 10 stuks per doos
aantalVruchtenPerDoosje = 10;
// en het aantal doosjes is..
return AantalDoosjes(
aantalVruchten,
aantalVruchtenPerDoosje);
}
catch(Exception ex)
{
string fout = ex.Message;
throw;
}
}
Functie VruchtenPerDoosje roept op zijn beurt weer een functie AantalDoosjes aan waarin aan de hand van het aantal vruchten en het aantal vruchten per doos wordt berekend hoeveel doosjes nodig zijn. Het gaat fout als het aantal vruchten per doos nul is. Functie AantalDoosjes gaat dan een deling doen door nul.
static int AantalDoosjes(
int aantalVruchten,
int aantalVruchtenPerDoos)
{
try
{
// Bereken het aantal doosjes
return
aantalVruchten /
aantalVruchtenPerDoos;
}
catch(Exception ex)
{
throw new Exception(
"Een fout in AantalDoosjes().\r\n" +
"De fout: " + ex.Message);
}
}
Het hoofdprogramma krijgt uiteindelijk van de onderliggende functies informatie terug en bij 45 kersen zou deze tekst afgedrukt moeten worden: “We hebben voor 45 kersen 3 doosjes nodig.” (uitgaande van 15 kersen per doos).
Het hoofdprogramma krijgt echter een foutmelding terug. Het gaat ergens fout in de keten… We gebruiken een ex.StackTrace om te zien waar het in de keten fout gaat en we zien dat het in de broncode bij regel 73 fout gaat:
Iets gaat fout...
Een fout in AantalDoosjes().
De fout: Attempted to divide by zero.
at Exceptioneel2.MainClass.AantalDoosjes
(System.Int32 aantalVruchten,
System.Int32 aantalVruchtenPerDoosje) [0x0000a] in
/.../Program.cs:73
at Exceptioneel2.MainClass.VruchtenPerDoosje
(System.String vrucht,
System.Int32 aantalVruchten) [0x0002c]
in /.../Program.cs:56
at Exceptioneel2.MainClass.Main
(System.String[] args) [0x0000b]
in /../Program.cs:16
Press any key to continue...
Throw
We krijgen in bovenstaand voorbeeld de hele keten in beeld, maar alle tussenschakels moeten dan ook een throw doen en geen throw ex. Als een tussenschakel een throw ex doet dan krijg je alleen de keten te zien vanaf de tussenschakel. In dit voorbeeld dus alleen dat het bij regel 56 en 16 niet goed gaat, maar er wordt niets gezegd over regel 73:
Iets gaat fout...
Een fout in AantalDoosjes().
De fout: Attempted to divide by zero.
at Exceptioneel2.MainClass.VruchtenPerDoosje
(System.String vrucht,
System.Int32 aantalVruchten) [0x0002c]
in /.../Program.cs:56
at Exceptioneel2.MainClass.Main
(System.String[] args) [0x0000b]
in /.../Program.cs:16
Press any key to continue...
Slot
In deze post hebben we laten zien hoe via exceptions omgegaan kan worden met situaties waarin niet is voorzien. Het programma stopt er weliswaar mee, maar we kunnen een foutmelding wat gebruikersvriendelijker weergeven. De gebruiker hoeft de foutmelding dan niet te ervaren als een computer crash met gierend uit de bocht vliegende programma’s die een verschrikkelijke dood sterven en met onbegrijpelijke foutmeldingen ter ziele gaan.
De uitverkorene die de fout mag gaan oplossen heeft niks aan gebruikersvriendelijke foutmeldingen. Hij / zij wil detail info en die informatie kan verkregen worden via een stack trace. In het voorbeeld zien we dat de stack trace je naar functie AantalDoosjes leidt, maar de fout wordt uiteindelijk veroorzaakt door functie VruchtenPerDoosje want in die functie is gedefinieerd dat een doosje nul kersen mag bevatten en dat gaat weer fout als we in functie AantalDoosjes proberen te delen door het aantal kersen per doos, zijnde nul.
Moraal van het verhaal: kersen zijn lekker, vooral als ze per 15 stuks in een doosje verkocht worden en met een stack trace krijg je weliswaar belangrijke aanwijzingen, maar het blijven aanwijzingen. Aan de aanwijzingen heb je misschien al voldoende om de fout te vinden, maar voor het echte zoek- en speurwerk zul je uiteindelijk toch een debugger nodig hebben. Je zal, gecombineerd met de aanwijzingen van de stack trace, uiteindelijk de bug wel vinden.
Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…