In het begin kan het onduidelijk zijn wanneer je problemen best afhandelt met klassieke conditionele code (met andere woorden, if
en verwanten) en wanneer met exceptions. Je vertrekt best van uit twee vragen:
Welke code kan het probleem tijdig vaststellen?
Welke code kan het probleem oplossen?
Als de code die het probleem kan vaststellen het probleem ook kan oplossen, heb je geen exception handling nodig. Volgende code bevat twee problemen die de code wel kan vaststellen en één probleem dat ze niet tijdig kan vaststellen:
De nulde verjaardag en de verjaardagen vanaf 126 kan onze code tijdig zelf detecteren. Dat is gewoon een vergelijking met een getal. Een probleem met het formaat doet zich pas voor wanneer de conversie plaatsvindt. Dan is het al te laat. Dan kunnen we alleen het probleem nog oplossen.
Kan je een fout goed oplossen met if
? Doe dat dan. Maar gebruik een exception als de code die het probleem tijdig kan vaststellen niet dezelfde is die het probleem kan oplossen.
Een methode is, in essentie, een stappenplan. Een stappenplan kan niet altijd rekening houden met elke mogelijke situatie. Soms treden er uitzonderlijke situaties op waarin het plan niet meer gevolgd kan worden. Het programmeerconcept dat overeenstemt met zo'n "uitzonderlijke situatie" is de exception (Engels voor "uitzondering").
Een uitzonderlijke situatie kan niet altijd verholpen worden door het stappenplan uit te breiden. Code die een deling implementeert kan bijvoorbeeld gewoonweg niet verder als je ze gebruikt om door het getal 0 te delen. Een methode als File.ReadAllLines
kan zelf niet weten wat ze moet doen als de file die je wil lezen niet bestaat: moet er gebruik gemaakt worden van default data, moet de gebruiker gevraagd worden om de file eerst in te vullen,...? Er is geen eenduidig antwoord. Het hangt af van de context waarin de methode gebruikt wordt. Dat wil zeggen: de code die, rechtstreeks of onrechtstreeks, de code heeft opgeroepen waarin de uitzonderlijke situatie is opgetreden.
Exceptions staan toe de verantwoordelijkheid voor het afhandelen van deze uitzonderlijke situatie, dus de exception, te verschuiven naar de context. Zonder het mechanisme te geven, schetst onderstaande code een mogelijke probleemsituatie en mogelijke oplossingen:
Misschien is optie 1 beter voor jouw programma, misschien is optie 2 beter voor jouw programma. De auteur van File.ReadAllLines
kan dat niet voorspellen en kan dus zelf het probleem niet afhandelen. Het probleem moet afgehandeld worden in StartProgrammaOp
.
Je zal zelf waarschijnlijk al exceptions zijn tegengekomen in je console programma's. Wanneer je je programma gewoon uitvoert en er plots een hele hoop tekst verschijnt (met ondere andere het woord Exception in), gevolgd door het prompt afsluiten ervan, dan heb je een exception gegenereerd die je niet hebt afgehandeld.
Vooral het eerste zinnetje van zo'n exception is vaak verhelderend. Hier wordt duidelijk aangegeven dat de gezochte file niet bestaat.
Indien je aan het debuggen bent en je krijgt een exception dan zal deze anders getoond worden, maar het gaat wel degelijk om dezelfde fout:
Het mechanisme om exceptions af te handelen in C# bestaat uit 2 delen:
Een try
blok: dit is de context waarin een mogelijke exception verwacht wordt
Een of meerdere catch
-blokken: dit blok zal exceptions die in het bijhorende try-block voorkomen afhandelen. Met andere woorden: in dit blok staat de code die de uitzondering zo goed mogelijk zal verhelpen.
De syntax is als volgt:
Merk op dat het catch
-blok meteen na het try
-blok komt, vergelijkbaar met hoe een else
-blok meteen na een if-blok
komt.
Als methode A (zonder geschikt catch
-blok) methode B oproept en B een exception genereert, moeten we kijken naar de bredere context waarin B is opgeroepen. Dat kan een methode C zijn die A heeft opgeroepen. Een foutmelding zoals in de figuren hoger op deze pagina zie je als er geen context bestaat waarin de exception goed wordt afgehandeld.
Vergelijk deze situatie met een hiërarchisch georganiseerde werkvloer. Veronderstel dat er drie niveaus zijn:
een directeur
een departementsmanager
een bediende
Wat als de bediende tijdens zijn werkdag een belangrijke fout in de boekhouding ontdekt, die hij zelf niet mag rechtzetten? Dan meldt hij dat aan zijn manager. Misschien kan de manager de fout rechtzetten als het gaat om iets dat met zijn eigen departement te maken heeft. Misschien ook niet. Dan moet hij de fout melden aan de directeur, die dan moet beslissen wat er mee moet gebeuren. Als dat niet lukt, kan het bedrijf zware schade oplopen of failliet gaan.
Vervang de directeur door methode C (bijvoorbeeld Main
), de departementsmanager door methode A (een methode die wordt opgeroepen van uit methode C) en de bediende door methode B. Dan krijg je het mechanisme achter exceptions.
In volgend stukje code kunnen uitzonderingen optreden:
Een FormatException
zal optreden wanneer de gebruiker tekst invoert of wanneer een komma-getal wordt ingevoegd. De conversie verwacht dit niet. Convert.ToInt32()
kan enkel werken met gehele getallen.
We tonen nu hoe we dit met exception handling kunnen opvangen:
Indien er nu een uitzondering optreedt dan zal de tekst “Verkeerde invoer” getoond worden. Vervolgens gaat het programma verder met de code die mogelijk na het catch-blok staat. Merk ook op dat we het try
-blok zo klein mogelijk gemaakt hebben door het enkel rond de conversiestap te zetten. Het is een goede gewoonte de context waarin een exception kan optreden zo nauwkeurig mogelijk af te bakenen. Maak je try
-blokken zo klein als nodig is het gewenste gedrag uit je programma te krijgen, maar niet kleiner.
catch
-blokkenException
is een klasse van het .NET framework. Er zijn van deze ouderklasse meerdere exception-klassen afgeleid die een specifieke probleemsituatie beschrijven. Enkele veelvoorkomende zijn:
Klasse
Omschrijving
Exception
Basisklasse. Erg breed, dus je gebruikt beter specifiekere exception klassen in je catch blokken.
SystemException
Ouderklasse van ingebouwde exceptions. Erg breed, dus je gebruikt beter specifiekere exception klassen in je catch blokken.
IndexOutOfRangeException
De index is te groot of te klein voor de benadering van een array.
NullReferenceException
Benadering van een niet-geïnitialiseerd object. Deze zie je bijvoorbeeld als je een objectmethode oproept van een variabele met waarde null
.
ApplicationException
Een ouderklasse voor exceptions die je zelf definieert en die een specifiek soort probleem in jouw applicatie aangeven.
Je kan in het catch blok aangeven welke soort exceptions je wil vangen in dat blok. In het voorbeeld hiervoor stond:
Hiermee vangen we dus alle Exceptions op, daar alle Exceptions van de klasse Exception
afgeleid zijn en dus ook zelf een Exception
zijn. Dit is een vorm van polymorfisme: één type data kan meerdere concrete vormen aannemen.
We kunnen nu echter ook specifieke exceptions opvangen. Wanneer je meerdere catch
-blokken hebt, wordt het eerste eerst toegepast indien mogelijk. Daarom moet je eerst catch
-blokken voor specifiekere exceptions voor blokken voor algemenere exceptions plaatsen. Stel bijvoorbeeld dat we weten dat de FormatException
kan voorkomen en we willen daar iets mee doen. Volgende code toont hoe dit kan:
Indien een FormatException
optreedt dan zal het eerste catch-blok uitgevoerd worden, anders het tweede. Het tweede blok zal niet uitgevoerd worden indien een FormatException
optreedt.
De MSDN bibliotheek is de manier om te weten te komen welke exceptions een methode mogelijk kan gooien. Gaan we bijvoorbeeld naar de pagina van de ReadAllLines
methode van de File
klasse, dan zien we onder "Exceptions" een aantal scenario's waarin het kan foutlopen en hoe deze gesignaleerd worden.
Herinner je uit het hoofdstuk rond geheugenbeheer dat elke methode-oproep data op de stack plaatst, het "snelle programmageheugen". Dus als methode A methode B oproept en methode B roept methode C op, krijg je een stack die er als volgt uitziet:
informatie over methode C
informatie over methode B
informatie over methode A
Hier is een belangrijke link met exceptions: de methodes die op een gegeven moment op stack worden opgevolgd, vormen de context waarin een exception kan optreden. Als er een exception optreedt in C is het aan B om die af te handelen. Lukt dat niet, dan is het aan A. Wanneer er een exception optreedt, krijg je een gedeeltelijke weergave van de stack te zien. Deze weergave heet de stack trace. Ze is een erg nuttig hulpmiddel als je probeert op te sporen waar een exception precies vandaan komt.
De Exceptions die worden ‘gegooid’ door het programma zijn objecten van de Exception-klasse. Deze klasse bevat standaard een aantal interessante properties en methoden, die je kan oproepen in je code.
Bovenaan de declaratie van het catch-blok geef je aan hoe het exception object in het blok zal heten. Je kent de exception dus toe aan een variabele. In de vorige voorbeelden was dit altijd e
.
Omdat alle exception van Exception afgeleid zijn bevatten ze allemaal minstens:
Element
Omschrijving
Message
Foutmelding in relatief eenvoudige taal
StackTrace
De weergave van de stack die je vertelt hoe de exception is ontstaan.
TargetSite
Methode die de exception heeft gegenereerd. Dit is de onmiddellijke context waarin de exception is opgetreden. Ze staat ook bovenaan de strack trace.
ToString()
Geeft het type van de exception, Message en StackTrace terug als string.
We kunnen via deze parameter meer informatie uit de opgeworpen uitzondering uitlezen en bijvoorbeeld aan de gebruiker tonen:
Opgelet: vanuit security standpunt is het zelden aangeraden om Exception informatie zomaar naar de gebruiker te sturen. Mogelijk bevat de informatie gevoelige informatie en zou deze door kwaadwillige gebruikers kunnen misbruikt worden!
Sommige zaken moeten sowieso gebeuren in je programma, of er nu een fout is opgetreden of niet. Een voorbeeld: je opent een databaseverbinding via C# om zo bepaalde data uit de database te lezen. Het blijkt dat de uitgevoerde query geen resultaat oplevert. Dit leidt tot een exception, omdat je programma verwacht dat de opgevraagde data aanwezig is in het systeem. Of deze fout zich nu voordoet of niet, achteraf moet de databaseconnectie gesloten worden.
Dit kan met het woordje finally
. finally
duidt een block aan dat sowieso wordt uitgevoerd. Als er geen exception is opgetreden, wordt dit block uitgevoerd na het try
block. Als er wel een is opgetreden, na het catch
block.
Volgend voorbeeld toont dit aan. Probeer het uit op je eigen machine:
Een finally
block voert bijna altijd uit. De enige situatie waarin het niet uitvoert, is als je programma stopt terwijl de try of bijbehorende catch nog niet volledig is afgewerkt. Dit kan bijvoorbeeld zijn omwille van een oproep van de methode Environment.Exit
of omdat je catch block zelf een exception oplevert die niet wordt afgehandeld en die zo ernstig is dat het controlemechanisme van C# in de war raakt.
Het is moeilijk op voorhand duidelijk te maken welke exceptions ernstig genoeg zijn om het controlemechanisme van C# in de war te brengen. Volgens de officiële documentatie is het in de meeste situaties ook niet erg belangrijk wat je programma doet nadat het gecrasht is.
"Maar de code hierboven werkt ook zonder finally
!" In dit geval wel. Maar finally
is "krachtiger" dan code die gewoon achter alle catch
blokken staat. finally
voert altijd uit, tenzij het programma volledig afsluit. Zelfs na een return
of na een handler op hoger niveau.
Je krijgt code die een exception oplevert, maar je kan deze deze oplossen zonder exception handling.
Maak eerst een klasse ExceptionHandling met een methode ToonSubmenu zodat je je oefeningen rond exception handling kan demonstreren. Voeg dan volgende methode toe:
Verbeter zelf de fout.
Start terug vanaf de code van eerder. Noem ze ditmaal DemonstreerFoutafhandelingWeekdagenMetException
. Los nu het probleem op, enkel en alleen door exception handling toe te voegen op de juiste plaats. De voorbeeldinteractie blijft identiek dezelfde.
Je krijgt opnieuw code die een exception oplevert, maar je kan deze deze oplossen zonder exception handling.
Start vanaf volgende code:
Spoor zelf de fout op en pas de code aan zodat ze hetzelfde doet, zonder gebruik te maken van exception handling. Gebruik eventueel de debugger.
Start terug vanaf de code van eerder. Los nu het probleem op door te vermelden wat er is misgelopen met behulp van exception handling. Noem je methode nu DemonstreerFoutAfhandelingOverflowMetException
.
Schrijf een programma dat een array maakt met drie willekeurige gehele getallen in en de gebruiker toestaat om een getal naar keuze te tonen, tot hij klaar is.
Eerst maak je de array aan. Daarna start je een bepaald soort lus op. Kijk hiervoor in de voorbeeldinteractie welke stappen zich steeds herhalen. Als je programma werkt wanneer de gebruiker zich netjes aan de regels houdt, voeg je exception handling toe om rekening te houden met verkeerde indexwaarden. Op andere soorten exceptions wordt niet voorzien. Noem de methode hiervoor DemonstreerKeuzeElement
.
Bij de vorige oefening zijn er nog randsituaties mogelijk. Handel de meest waarschijnlijke op een specifieke manier af en voorzie een algemeen vangnet.
Test voorgaande code uit met tekst in plaats van een getal. Test voorgaande code uit met een enorm groot getal. Test voorgaande code uit met een kommagetal. Test voorgaande code uit door meteen op enter te duwen. Test voorgaande code uit met een dollarteken in plaats van een getal. Onthoud de soorten exceptions.
Voorzie vervolgens exception handling om uit te leggen wat er is misgelopen zonder het programma te laten crashen, zoals je in de vorige oefening ook hebt aangegeven dat een bepaalde index niet geldig was.
Voorzie ook code om om het even welk type exception af te handelen.
Schrijf een klasse Kat
. Deze klasse encapsuleert onze domeinkennis over katten en zorgt ervoor dat we geen onrealistische katten kunnen voorstellen in een softwaresysteem.
Maak de klasse. Deze beschikt over een property Leeftijd
, met een publieke getter, maar geen publieke setter. De leeftijd wordt meegegeven bij constructie en wordt ingesteld, maar als hij hoger is dan 25, moet de code die het Kat
-object heeft proberen aanmaken een ArgumentException
afhandelen, met de boodschap: "Deze kat is te oud!".
Pas ook onderstaande code (die je in een methode DemonstreerLeeftijdKat
van ExceptionHandling
mag plaatsen) aan zodat deze boodschap wordt geprint, maar je programma niet crasht:
Schrijf code die op willekeurige wijze een lijst met katten aanmaakt. Dit kan mis lopen. Hoe dan ook moet je code netjes achter zich opkuisen door in alle gevallen deze lijst terug leeg te maken wanneer het werk gedaan is.
Dit is een nogal vreemd voorbeeld, maar we hebben in deze cursus niet gezien hoe je met databaseconnecties, streams, e.d. werkt en dat zijn het soort zaken die je typisch opkuist in alle mogelijke scenario's.
Maak een methode DemonstreerLeeftijdKatMetResourceCleanup
Maak in deze methode een lijst met katten
Voeg twintig katten met een willekeurige leeftijd van 0 tot 30 toe aan deze lijst
Als dit zonder problemen verloopt, toon je: "De volledige lijst met katten is aangemaakt!"
Als er ergens een probleem optreedt omwille van een ongeldige leeftijd, toon je: "Het is niet gelukt :-("
In beide gevallen zorg je dat de methode eindigt door te lijst terug leeg te maken met de methode Clear
Doe dit op zo'n manier dat dit altijd gebeurt, ook als er een andere exception dan de ArgumentException
optreedt en deze exception op een hoger niveau wordt opgevangen
OF
We willen een utility methode schrijven om makkelijk files te lezen.
Schrijf een methode FileHelper
. Deze vraagt eerst om een pad naar een file en probeert deze file te lezen. Als dit lukt, geeft ze heel de inhoud van de file terug als string. Als de file niet bestaat, geeft ze nog altijd een string terug (geen exception!) met de waarde: "File kon niet gevonden worden." In het geval van andere problemen met input/output, geeft ze ook een string terug, met waarde: "File bestaat, maar kon niet gelezen worden. Mogelijk heb je geen toegangsrechten." In nog algemenere problemen geeft ze een string terug met waarde: "Er is iets misgelopen. Neem een screenshot van wat je aan het doen was en contacteer de helpdesk."
OF
OF
We doen een uitbreiding op h16-leeftijd-kat. We zouden graag makkelijk in detail kunnen uitleggen aan de gebruiker waarom het is misgelopen. We doen dit hier in de eerste plaats door een custom exception type te voorzien.
Maak een kopie van je klasse Kat
. Noem deze KatMetCustomException
.
Maak een klasse KatLeeftijdException
. Deze erft van ArgumentException
.
Ze heeft drie read-only properties, waarvan je zelf het juiste type zou moeten kunnen bepalen:
MeegegevenWaarde
LaagstMogelijkeWaarde
HoogstMogelijkeWaarde
Ze heeft een constructor die (enkel) waarden voor deze drie properties als parameters heeft.
Wanneer de leeftijd van een KatMetCustomException
wordt ingesteld, wordt een exception van dit type in plaats van een ArgumentException
gegooid. Hierbij vul je de argumenten in op basis van de rest van je code.
Maak ook een variatie op je eerdere demonstratiemethode. Noem deze DemonstreerLeeftijdKatMetCustomException
.
Als dit in je code staat:
Dit bericht mag niet "hardgecodeerd zijn". Elk getal moet uit de exception gehaald worden.
Maak in je SchoolAdmin project een klasse DuplicateDataException
. Deze heeft twee properties, Waarde1
en Waarde2
, beide van type System.Object
. Ze heeft ook een constructor die een message en de twee waarden als parameter heeft.
Wanneer je een nieuwe cursus aanmaakt, wordt deze vanzelf geregistreerd in het systeem. Pas je code aan zodat geen twee cursussen met dezelfde naam kan registreren. Meerbepaald: zorg dat een poging om een cursus aan te maken afgebroken wordt door middel van een DuplicateDataException
vooraleer de teller van alle cursussen wordt verhoogd. De boodschap die je meegeeft is: "Nieuwe cursus heeft dezelfde naam als een bestaande cursus." Voor de eerste waarde geef je de nieuwe cursus, voor de tweede geef je de bestaande cursus.
Zorg er ook voor dat je keuzemenu niet crasht wanneer deze fout zich voordoet, maar de boodschap van de exception toont en het ID van de bestaande cursus waarmee de nieuwe cursus zou overlappen. Dit kan je doen door Waarde2
te casten.
Het is niet logisch een inschrijving te hebben zonder student of zonder vak. Zorg ervoor dat een VakInschrijving niet kan aangemaakt worden zonder een (of beide) van deze elementen. Gebruik hiervoor een ArgumentException
. Breid bij wijze van demonstratie je keuzemenu om een student of een vak toe te voegen uit met een optie met nummer 0 om de waarde null
te gebruiken. (Dit zou je in het echt niet toevoegen aan je systeem zelf, maar je zou aparte testcode schrijven die dit doet.) Zorg ook dat het niet toegelaten is een student twee keer in te schrijven voor hetzelfde vak. Ook dat levert een ArgumentException
. Zorg dat het keuzemenu niet crasht wanneer je deze optie kiest, maar gewoon de boodschap van de exception toont.
Er mogen niet meer dan 20 lopende inschrijvingen per cursus zijn. Zorg ervoor dat er een CapaciteitOverschredenException
(met enkel de message als parameter) optreedt wanneer je iemand probeert in te schrijven voor een cursus waarvoor al 20 inschrijvingen (zonder toegekend resultaat) bestaan. Zorg ervoor dat je keuzemenu hierop voorzien is en de message toont, zonder te crashen.
We schrijven flexibele formuliercode, die we ook zullen gebruiken om de duidelijkheid van onze formulieren te verbeteren. Een formulier logt ongeldige antwoorden op vragen vooraleer het de fout signaleert aan een hoger niveau.
Dit is een uitdagende, maar leerrijke en realistische oefening.
We vertrekken vanaf dit klassendiagram:
Je krijgt ook volgende demonstratiecode om in het submenu van je klasse ExceptionHandling
te plaatsen:
De werking van elke klasse is als volgt:
FormulierVraag:
Dit stelt één vraag op één formulier voor, inclusief het antwoord dat eventueel al is gegeven op deze vraag.
De tekst is de vraag waarop een antwoord verwacht wordt. Deze mag nooit leeg of null zijn.
Het antwoord is het antwoord dat de invuller gegeven heeft op deze vraag, in tekstformaat. Dit moet initieel null zijn maar mag later nooit meer naar null gewijzigd worden.
Het presenteren van een vraag en het inlezen van een antwoord hangt af van het vraagtype, omdat elk vraagtype eigen instructies heeft (bv. antwoorden in tekst of met een reeks cijfers,...)
FormulierGetalVraag:
Dit stelt een vraag voor waarbij een getal wordt verwacht.
De ondergrens is het kleinste getal dat mag worden ingegeven, de bovengrens is het grootste getal dat mag worden ingegeven.
Als een vraag van dit type wordt aangemaakt met een ondergrens die groter is dan de bovengrens, krijgen we een ArgumentException
.
Bij het inlezen van een antwoord wordt de ingetypte tekst geconverteerd naar een getal. Als dit getal tussen de ondergrens en bovengrens ligt, wordt het antwoord (het getal, voorgesteld als string) opgeslagen. Anders wordt gesignaleerd dat het antwoord tussen deze twee getallen moet liggen en wordt er opnieuw tekst ingelezen, tot er een antwoord verkregen is (of er een exception optreedt).
Tonen van een vraag gaat als volgt:
Eerst wordt de vraagtekst geprint.
Daaronder wordt toegevoegd: "Dit is een getal tussen ... en ..." (met daar de grenzen ingevuld)
FormulierVrijeTekstVraag:
Alle tekst is geldig als antwoord
Tonen van een vraag: de vraagtekst wordt getoond. Daaronder wordt getoond: "Sluit af met ENTER."
Formulier:
Bij constructie wordt er een lijst met FormulierVraag-objecten meegegeven.
Deze vragen worden opgeslagen en na aanmaak van het formulier kan de lijst met vragen niet meer gewijzigd worden.
Een formulier invullen betekent dat we één voor één elke vraag in het formulier tonen en het antwoord inlezen.
Dit kan fout lopen. Als er iets fout loopt (wat dan ook), tonen we een bericht "Onverwachte fout wordt naar schijf weggeschreven." en staan we vervolgens toe dat de fout naar een hoger niveau van de programmacode gaat.
Een formulier tonen betekent dat we voor elke vraag in het formulier de tekst van de vraag en het opgeslagen antwoord tonen.
Je kan ook in je eigen code uitzonderingen genereren, zodat deze elders opgevangen worden. Je kan hierbij zelf exceptions maken of gewoon gebruik maken van een bestaande Exception
-klasse.
Een voorbeeld:
"Getal is 0. Dit is niet voorzien."
is dus de boodschap die we toevoegen aan onze exception. Ze wordt "opgelost" door de boodschap gewoon te tonen. In een complexer programma zou je bijvoorbeeld de waarde van de input kunnen aanpassen en dan opnieuw de methode aanroepen.
Je kan ook eigen klassen afleiden van Exception
zodat je eigen uitzonderingen kan maken en gooien in je programma. Je maakt hiervoor gewoon een nieuwe klasse aan die je laat overerven van de ApplicationException
-klasse. Een voorbeeld:
Om deze exception nu zelf op te gooien gebruiken we het keyword throw
. In volgende voorbeeld gooien we onze eigen exception op een bepaald punt in de code en vangen deze dan op:
Technisch gezien kan je ook rechtstreeks erven van Exception
en SystemException
, maar in de documentatie staat uitdrukkelijk dat je eigen klassen best afleidt van ApplicationException
.
Omdat dit een oefening op het basisgebruik is, wijken we hier af van .
Overdrijf niet met eigen Exceptions. Op vind je, onder "Derived", een heleboel kant-en-klare exceptions voor allerlei situaties.