Exceptions / Undantag
Exceptions, eller undantag som det heter på svenska, skapar man då
man stöter på något som är så fel eller ovanligt att man inte kan
fortsätta med det man skulle göra. I språk som Java säger man att
man kastar undantag (throw), i andra språk kan det heta raise.
Man höjer alltså en flagga för att signalera att metoden inte
avslutades så som den normalt brukar avslutas.
Om man till exempel ska skriva något i en fil men upptäcker att
filen inte finns eller att disken är full så är det inte säkert
att metoden som ska skriva till filen vet hur den ska hantera
denna märkliga situation. Den kastar då ett undantag till den som
anropade metoden och låter anroparen besluta vad som ska göras.
Undantag skickas inte via de vanliga returvärdena från en metod,
utan normalt finns en separat kanal för att hantera dessa. När
ett undantag kastas kommer koden därefter att hoppas över. Man
hoppar frammåt i programmet tills dess att man hittar en speciell
kodbit som hanterar felet som uppstått. Det är vanligt att
programmeringsspråk har ett speciellt sätt att kapsla in kod som
kan generera undantag så att det vet direkt var någonstans koden
som hanterar det finns. Om koden för att hantera undantaget finns
i samma metod som det genereras så hanteras det lokalt, annars
kommer metoden att avslutas och undantaget skickas vidare till den
anropande koden.
Exempel i Java
try {
FileOutputStream foStr = new FileOutputStream("foo");
}
catch (FileNotFoundException e) {
System.err.println("File not found!");
}
Hur och var undantag bör hanteras
Mitt generella tips när det gäller var man ska fånga undantag är:
Fånga och hantera undantag så tidigt som möjligt. Om du kan
hantera undantaget i samma metod som orsakar det så ska du fånga
det där. Skicka inte vidare undantag som kod utanför metoden inte
behöver känna till. Att läcka undantag är att avslöja
implementationsdetaljer, och det ska man aldrig göra.
Om du försöker öppna en fil och den till exempel inte finns kommer
du få ett FileNotFoundException från Java. Om detta
inträffar i en metod där du har möjlighet att åtgärda felet (till
exempel genom att skapa filen eller öppna någon annan fil) så är
detta rätt ställe att ta hand om det. Fånga det då enligt exemplet
ovan och gör rätt sak. Det är i princip aldrig rätt sak att
ignorera undantag genom att fånga dem och lämna hanteringskoden
tom. Undantag genereras för att något har gått snett, att ignorera
detta är nästan alltid en bugg.
Om det inte går att hantera undantaget på något vettigt sätt i den
metod där det genereras så bör det skickas vidare till den
anropande koden. I det fallet fångar man inte undantaget alls,
utan deklarerar att metoden kan kasta ett undantag med
throws .
public void readFile() throws FileNotFoundException
{
FileOutputStream foStr = new FileOutputStream("foo");
...
}
Om man ska sätta en hel metod inom try{} eller bara
det specifika anrop som genererar undantaget beror helt på vad
metoden gör i övrigt och hur den ser ut rent estetiskt. Om man har
flera anrop som kan generera undantag och alla dessa ska hanteras
på samma sätt så är det ju vettigt att sätta alla dessa anrop inom
samma try{} så att man bara behöver skriva
hanteringskoden på ett ställe. Om det är flera anrop som genererar
olika undantag som ska hanteras på olika sätt så tycker inte jag
att de ska sitta i samma try{} även om det är möjligt
i de flesta fall. Egentligen handlar det nog mest om en
koddesign-fråga om man vill sätta hanteringskoden nära stället där
undantaget genereras eller om man vill sätta all hanteringskod för
en hel metod i slutet av metoden. Det viktigaste är ju att koden
blir lättläst, så om det stökar till i koden att sätta en massa
hanteringskod överallt så är det kanske bättre att sätta allt sist
genom att sätta hela metoden i samma try{} .
När ska man använda undantag?
Att kasta undantag är som namnet antyder något man endast gör i
undantagsfall. Fel som kan hanteras lokalt bör också hanteras
lokalt, man ska inte lasta över ansvaret på någon annan metod om
det inte är nödvändigt. Detta betyder även att man inte ska
hantera andras fel. Får man till exempel felaktiga indata så är
det högst motiverat att tala om att detta skedde, även om man kan
hantera felet själv. Felaktiga indata är oftast något som den
anropande metoden vill veta om eftersom det antyder att något är
fel på riktigt. Att kasta ett undantag är ett utmärkt sätt att
hantera felaktiga indata.
Det finns två regler man aldrig bör bryta emot:
Om man inte vet varför ett undantag uppstår är det ganska troligt
att man inte kan hantera det på ett vettigt sätt. Om man får ett
undantag tillbaka från ett metodanrop så betyder det att något
gått fel där. En felaktig hantering av detta undantag (till
exempel att ignorera det) är det samma som att gömma fel i
programmet. Kan man inte hantera felet är det bättre att skicka
det vidare.
Som utvecklare är det ganska vanligt att man sitter längst ut i
anropskedjan. Det vill säga man använder biblioteksfunktioner som
kan generera undantag, men det finns ingen anropande kod utanför
den egna som kan hantera felen. Regeln gäller naturligtvis även då
- fånga inte undantaget om du inte vet varför det uppstår. Men då
det inte finns någon att skicka vidare felet till blir du så illa
tvungen att själv ta reda på vad som faktiskt händer och hantera
undantaget på riktigt. Den som ser detta som en dålig sak kan
sluta läsa nu och bör överväga att låta bli att programmera.
Den andra regeln är också till för att förhindra att man gömmer
fel i sitt program. Säg till exempel att man (i Java) vill ta hand
om FileNotFoundException . Då är det just detta man
ska fånga, inte det mer generella IOException vilket
kanske kan kännas lockande ibland. Att fånga
IOException och bara hantera felet att filen inte
hittas gör att saker kommer att gå sönder om andra I/O-fel
inträffar. Av samma skäl fångar man därför aldrig
RuntimeException eller Exception .
Regel två vill även förhindra att man fångar undantag av typen
NullPointerException och
ArithmeticException . Dessa indikerar att man har ett
fel i sitt program och man ska inte fånga dem, man ska se till att
de aldrig uppstår! NullPointerException får man om
man försöker följa en referens som är NULL och
ArithmeticException får man till exempel vid division
med noll.
Man ska alltid se till att hantera de fel som man vet kan uppstå.
Innan man utför en division ska man alltid försäkra sig om att man
inte har noll i nämnaren. Om man upptäcker att det är en nolla så
kan det mycket väl vara rätt sak att hantera detta genom att
generera ett undantag. Men det ska då vara ett som talar om att
den som skickade in nollan gjorde fel, inte
ArithmeticException som säger att du gjorde fel som
försökte använda nollan i en division.
Ibland ser man kod där undantag används som ett sätt att styra
kontrollflödet i programmet. Man kastar undantag i vanligt
förekommande situationer enbart därför att man inte orkar tänka ut
en riktig lösning på ett problem. Detta är (enligt mig) fel och är
ett exempel på dålig programmdesign.
Tillbaka till indexet |