Gå til indholdet

Fejlhåndtering

Der vil altid kunne opstå fejl i dine applikationer!

Det kan enten skyldes, at du skriver kode, der fejler, eller udefrakommende påvirkninger som en bruger, der indtaster noget forkert, en database, der pludselig ikke svarer, eller en fil der ikke længere eksisterer. Derfor er det vigtigt at kunne fange og håndtere fejl.

Information til undervisere

Udover at kursisten naturligvis skal vide hvordan man fanger fejl, og at man kan fange fejl globalt, er det ligeså vigtigt at vide hvornår det er nødvendigt. Hvis man ikke logger, viser fejlen på brugerfladen eller beriger en fejl med yderligere informationer - er det jo faktisk bedre at lade runtime fejle. Fejlhåndtering koster.

Og det her er en kæmpe fejl:

try
{
    // kode der kan fejle
}catch(Exception ex)
{

}

Fejlen bliver bare spist og forsvinder ud i ingenting.

Bugs

Når du hører om softwarefejl, vil du tit høre dem omtalt som bugs (engelsk for insekter), hvilket kan lyde lidt mystisk. Men begrebet skulle komme fra en reel hardwarefejl i den gamle Mark II relæcomputer fra 1946, hvor et møl var kommet i klemme i et relæ og dermed skabte en fejl. Insektet blev fundet, klistret ind i en log og er i dag udstillet på National Museum of American History:

Billede af den første bug

Exceptions

I C# hedder en fejl en exception, og den kan enten skabes af runtime, 3. parts kode eller din egen kode. I daglig tale vil du høre, at der bliver smidt en fejl, og det skal du forstå, som at applikationen afbrydes, og årsagen kan aflæses i et Exception-objekt. Din opgave er så at fange Exception-objektet og eventuelt håndtere fejlen på en eller anden måde. Måske vil du blot fortælle brugeren på en pæn måde, at der er sket en fejl, måske vil du gentage nogle instruktioner, eller måske vil du logge fejlen i en database eller fil. Der er typisk tale om en kombination, men det er helt op til dig.

Info

I C# teori vil du høre om begreberne handled og unhandled exceptions. Det skal du forstå som håndterede (du skriver kode til at håndtere fejlen) og uhåndterede (runtime vil blot afbryde programmet) fejl.

Eksempler på fejl

For at du kan få en idé om forskellige typer af fejl, og eventuelt prøve dem af selv, er her et par eksempler:

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            // Forskellige typer af fejl

            // Fejl1();
            // Fejl2();
            // Fejl3();
            // Fejl4();

        }

        public static void Fejl1()
        {
            string tekst = null;
            // Vil generere en NullReferenceException
            // fordi tekst variablen ikke har en reference
            // til noget
            Console.WriteLine(tekst.ToUpper());
        }

        public static void Fejl2()
        {
            string indhold = 
               System.IO.File.ReadAllText(@"c:\temp\xyz.txt");
            // Vil generere en FileNotFoundException
            // hvis filen ikke eksisterer
        }

        public static void Fejl3()
        {
            checked
            {
                byte b = byte.MaxValue;
                b++;
            }
            // Vil generere en OverflowException
            // fordi 255 + 1 ikke "kan være" i en 
            // byte (og checked sørger for at smide
            // en fejl i så fald)
        }

        public static void Fejl4()
        {
            int a = 10;
            int b = 0;
            int res = a / b;
            // DivideByZeroException
        }
    }
}

Du bør prøve at skabe en ny konsol-applikation og prøve et par af fejlene af i praksis – både med og uden debugger. Hvis fejlen opstår med debuggeren tilknyttet, vil du opleve, at Visual Studio stopper, hvor fejlen er opstået, med information om et konkret Exception-objekt, som beskriver fejlen:

Exception i Visual Studio

Ved at klikke på View details kan du få oplysninger om den konkrete fejl:

Informationer om en Exception

Exception-objektet indeholder en masse brugbare informationer som eksempelvis:

Navn Forklaring
Message Den konkrete fejltekst
StackTrace I hvilken metode fejlen er opstået, og hvordan programpointeren er kommet derhen
Source I hvilket assembly fejlen er opstået
InnerException Hvis fejlen er sket grundet en anden fejl, kan den oprindelige fejl aflæses her

Brug af try/catch

Fejlhåndtering i C# sker ved at omkranse kode med en try/catch struktur og dermed prøve noget kode af. Skulle der ske en fejl i koden, kan den fanges i catch-delen af strukturen:

try
{
    // kode der skal testes for fejl
}
catch(Exception ex)
{
    // skulle der opstå en fejl ”fanges” den her
    // og oplysninger om fejlen kan findes i 
    // exception-objektet.
}

Koden, der testes for fejl, kan både være enkeltstående instruktioner eller metodekald ud af try-strukturen. Skulle der ske en fejl i en metode (eller én metode der kalder en anden) vil fejlen også blive fanget. Her er et eksempel på enkeltstående instruktioner og en fejlhåndtering, som blot udskriver en fejltekst:

try
{
    Console.WriteLine("Indtast tal");
    string talTekst = Console.ReadLine();
    double tal = Convert.ToDouble(talTekst);
    Console.WriteLine($"Du har indtastet {tal:N2}");
}
catch (Exception ex)
{
    Console.WriteLine($"Ups - følgende fejl er opstået: {ex.Message}");
}

Koden henter en tekst fra brugeren, og teksten konverteres til et tal. Det kan jo gå fint, hvis brugeren indtaster et tal som forventet, men det kan også gå galt, hvis brugeren indtaster noget vrøvl. I så fald smider ToDouble-metoden en fejl, som fanges i catch og udskriver en tekst:

Indtast tal
abc
Ups - følgende fejl er opstået: Input string was not in a correct format.

Du bestemmer naturligvis selv, hvor detaljeret du vil informere brugeren.

Her er et andet eksempel, som benytter et kald til en metode:

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Indtast tal");
                string talTekst = Console.ReadLine();
                int resultat = LægEnTil(talTekst);
                Console.WriteLine($"Resultatet er {resultat}");
            }
            catch (Exception ex)
            {
                Console.WriteLine(
                   $"Ups - følgende fejl er opstået: {ex.Message}");
            }            
        }

        static int LægEnTil(string talTekst) {
            int tal = Convert.ToInt32(talTekst);
            tal++;
            return tal;
        }
    }
}

Hvis brugeren indtaster noget, som ikke kan konverteres til et tal, fanges fejlen, og det sker altså på trods af, at fejlen opstår i en separat metode. Såfremt metoden er kaldt i en try-struktur, vil eventuelle fejl altid blive fanget.

Flere catch-blokke

I grundlæggende C# kan du nøjes med en simpel try/catch struktur med en enkelt catch-blok:

try
{
}
catch(Exception ex)
{
}

Men det er muligt at udvide antallet af catch-blokke med andre exception-typer således, at du kan skrive kode til at håndtere forskellige typer af fejl. Det gør det muligt at skrive kode til eksempelvis fejl relateret til filsystemet (måske vent et par millisekunder og prøv igen), eller fejl relateret til typekonvertering (måske bede brugeren om at indtaste data igen).

Den overordnede klasse er Exception-klassen, men i frameworket findes der en masse forskellige klasser, der arver fra Exception – herunder eksempelvis ArithmeticException, FormatException, IOException og mange flere:

Lille udsnit af forskellige Exception-klasser

Hvis du tilføjer flere catch-blokke, vil runtime sørge for at afvikle den korrekte blok kode afhængig af typen af fejl:

try
{
    // kald en metode hvor flere fejl kan ske
    MuligFejl();
}
catch (System.IO.IOException ex)
{
    // Gør noget
}
catch (NullReferenceException ex)
{
    // Gør noget
}
catch (ArithmeticException ex)
{
    // Gør noget
}
catch (Exception ex)
{
    // Gør noget
}

Du skal blot sørge for, at catch-blokken er placeret i en rækkefølge, der sikrer, at de mest specifikke typer står først. Hvis en fejl ikke passer på en specifik catch-blok, vil den altid passe ind i den sidste Exception-blok. Således vil du altid fange en fejl, hvis den sidste blok håndterer et Exception-objekt.

Du behøver ikke have mange catch-blokke i din kode. Du kan bare nøjes med en catch-blok, som er relateret til Exception-klassen. Så bliver alle fejl fanget.

Brug af finally

I nogen situationer kan det være praktisk at være sikker på, at instruktioner altid afvikles – både når der sker en fejl, og når der ikke sker en fejl. Derfor kan try/catch strukturen udvides med en finally-blok, som kan indeholde instruktioner til afvikling:

try
{
    // instruktioner 
}
catch(Exception ex)
{
    // instruktioner til at afvikling ved fejl
}
finally
{
    // instruktioner til afvikling hvad enten der sker en fejl eller ikke
}

Grunden til, at du skal benytte en finally blok, i stedet for blot at placere instruktioner efter try/catch strukturen er, at du er sikret at instruktioner i finally altid afvikles – også selv om instruktioner tvinger afvikling ud af try-blokken (eksempelvis return-kodeordet) eller der skulle ske en ny fejl i catch-blokken. Man benytter tit en finally-blok, hvis der arbejdes med ressourcer, hvor det er vigtigt, at der altid ryddes op efter brug. Det kunne eksempelvis være brug af filer eller databaser, hvor det ikke er smart, at en forbindelse kan risikere at blive efterladt åben.

Her er et kort eksempel, hvor der læses et antal bytes fra en fil. Selve IO koden er ikke væsentlig, men bemærk finally-blokken:

int index = 0;
string sti = @"c:\temp\test.txt";
System.IO.StreamReader file = new System.IO.StreamReader(sti);
char[] buffer = new char[10];
try
{
    file.ReadBlock(buffer, index, buffer.Length);
}
catch (Exception ex)
{
    // log fejl
    Console.WriteLine("Der er opstået en fejl!");
}
finally
{
    if (file != null)
        file.Close();
}

Ved at placere kald til Close-metoden i finally-blokken er du sikker på, at den altid bliver afviklet.

Opgaver

Skab dine egne fejl

At smide en Exception er en god måde at fortælle de udviklere, som benytter din kode (herunder dig selv), at der er sket en fejl, og det kan du gøre ved hjælp af throw-kodeordet.

Antag at du skal skabe en metode, der lægger to heltal tal sammen:

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            // dette er ok
            Console.WriteLine(LægSammen(10,10));
            // dette er ikke ok
            Console.WriteLine(LægSammen(1500, 10));
        }

        static int LægSammen(int a, int b) {
            return a + b;
        }
    }
}

Du skal nu sørge for, at metoden kun må lægge tal sammen, der er større end 0 og mindre end 1.000 – hvordan vil du løse den opgave?

Du kan ikke være sikker på, at den der kalder LægSammen-metoden, kender restriktionerne, så det er i selve metoden, du skal tilføje kode til at sikre, at logikken bliver overholdt. Du kunne måske gøre noget som eksempelvis:

static int LægSammen(int a, int b) {
    if ((a < 0 || a > 1000) || (b < 0 || b > 1000))
        return -1;
    else
        return a + b;
}

Nu vil metoden returnere -1, hvis a eller b har en forkert værdi, men det er en skidt løsning, fordi udvikleren, der kalder metoden, skal være bevidst om, at -1 er lig med en fejl og skal teste for en konkret fejl ved hvert kald. Bedre var det, hvis udvikleren, som kalder metoden, blot kan bruge en try/catch struktur og dermed fange eventuelle fejl. Det kræver dog, at LægSammen-metoden smider en fejl med en tilhørende Exception, og det kan som nævnt ske ved hjælp af throw-kodeordet:

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {

            try
            {
                // dette er ok
                Console.WriteLine(LægSammen(10, 10));
                // dette er ikke ok
                Console.WriteLine(LægSammen(1500, 10));

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static int LægSammen(int a, int b)
        {
            if ((a < 0 || a > 1000) || (b < 0 || b > 1000))
                throw new Exception("Argumenter har en forkert værdi");
            else
                return a + b;
        }
    }
}

Bemærk brugen af throw-kodeordet som smider en ny Exception med en sigende besked, og bemærk brugen af try/catch som fanger eventuelle fejl – herunder fejlen der opstår, når metoden kaldes med argumenterne 1500 og 10. Brugen af throw kan sidestilles med brugen af return på den måde, at det også er en måde at fortælle kompileren, at afvikling af metoden ønskes afbrudt.

Når du udvikler metoder, skal du have to kasketter på – én når du udvikler metoden, og én når du kalder metoden. Hvis du tænker på den måde, giver det lidt mere mening, at du smider en fejl i en metode, og tester for fejl ved kald.

I den mere avancerede C# kan du smide en fejl baseret på andre typer end Exception-klassen, men det er ikke så væsentligt på dette niveau.

Brug en snippet (try + ++tab tab++) for at få hjælp til at indsætte en try/catch struktur. Du kan også omkranse kode med en try/catch ved at bruge ctrl+K+S i Visual Studio og vælge try på listen over mulige snippets.

Opgaver

Log

Typisk vil fejlhåndtering involvere en eller anden form for log således, at der findes data til at genskabe fejlen.

Se Introduktion til log.