Izuzeci u C#: try, catch, finally i throw

Lekcija 10 od 15 · C#

Izuzetak (exception) je greška koja nastaje dok program radi: korisnik unese slova umesto broja, deli se nulom, traži se element van niza… Bez obrade, izuzetak prekida program. Blokovi try i catch omogućavaju da program grešku „uhvati“, obradi i nastavi rad.

Pre nego što počneš: potrebno predznanje – kolekcije, klase i objekti.
Kako da učiš: svaki primer prekucaj i pokreni, a zadatke na kraju prvo reši sam/a; uputstvo i rešenje otvori tek posle toga. Svi programi su konzolne aplikacije u C#-u (.NET 8 ili noviji, bez „top-level statements“); svaki je proveren i daje navedeni izlaz.

Hvatanje izuzetaka: try i catch

using System;

class Program
{
    static void Main()
    {
        Console.Write("Unesi broj: ");
        try
        {
            int x = int.Parse(Console.ReadLine());   // može baciti FormatException
            Console.WriteLine($"100 / {x} = {100 / x}");   // može baciti DivideByZeroException
        }
        catch (FormatException)
        {
            Console.WriteLine("Greska: to nije ceo broj.");
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("Greska: deljenje nulom.");
        }
        Console.WriteLine("Program nastavlja rad.");
    }
}

Kada u bloku try nastane izuzetak, ostatak bloka se preskače i traži se prvi catch čiji tip odgovara. Posle obrade program nastavlja iza celog try-catch. Najčešći izuzeci su prikazani u tabeli:

IzuzetakKada nastaje
FormatExceptiontekst nije u očekivanom obliku (int.Parse("abc"))
DivideByZeroExceptioncelobrojno deljenje nulom
IndexOutOfRangeExceptionindeks van granica niza
ArgumentOutOfRangeExceptionindeks van granica liste ili stringa
NullReferenceExceptionkorišćenje promenljive čija je vrednost null
KeyNotFoundExceptiončitanje ključa koji ne postoji u rečniku
OverflowExceptionbroj ne staje u tip (npr. int.Parse("99999999999"))

Blok finally i objekat izuzetka

Blok finally izvršava se uvek – i kada izuzetka nema i kada je uhvaćen. Koristi se za „pospremanje“: zatvaranje fajla, veze sa bazom… Iza tipa u catch može se navesti ime promenljive, pa se dobija objekat izuzetka sa podacima o grešci.

using System;

class Program
{
    static void Main()
    {
        int[] niz = { 10, 20, 30 };
        for (int i = 2; i <= 3; i++)
        {
            try
            {
                Console.WriteLine($"niz[{i}] = {niz[i]}");
            }
            catch (IndexOutOfRangeException e)
            {
                Console.WriteLine($"Uhvacen izuzetak: {e.GetType().Name}");
            }
            finally
            {
                Console.WriteLine($"finally za i = {i}");
            }
        }
    }
}

Svojstvo e.Message sadrži opis greške (na jeziku operativnog sistema), a catch (Exception e) hvata izuzetak bilo kog tipa – zato se piše poslednji, posle konkretnijih.

Izuzetak ili provera unapred?

Hvatanje izuzetaka je sporije i nepreglednije od obične provere. Kada se greška može predvideti – kao pogrešan unos – bolje je proveriti unapred, npr. metodom TryParse. Izuzeci su za situacije koje se ne mogu lako proveriti.

using System;

class Program
{
    // ponavlja unos dok korisnik ne unese ceo broj iz opsega [min, max]
    static int UcitajBroj(string poruka, int min, int max)
    {
        while (true)
        {
            Console.Write(poruka);
            if (int.TryParse(Console.ReadLine(), out int x) && x >= min && x <= max)
                return x;
            Console.WriteLine($"Unesi ceo broj od {min} do {max}.");
        }
    }

    static void Main()
    {
        int ocena = UcitajBroj("Ocena: ", 1, 5);
        Console.WriteLine($"Uneta ocena: {ocena}");
    }
}

Bacanje izuzetka: throw

I naš kod može da baci izuzetak naredbom throw, kada metoda dobije podatke sa kojima ne može da radi. Tako metoda ne mora sama da ispisuje poruke – greška se prijavljuje onome ko je metodu pozvao.

using System;

class Racun
{
    public decimal Stanje { get; private set; }

    public void Uplati(decimal iznos) => Stanje += iznos;

    public void Isplati(decimal iznos)
    {
        if (iznos <= 0)
            throw new ArgumentException("Iznos mora biti pozitivan.");
        if (iznos > Stanje)
            throw new InvalidOperationException($"Nema dovoljno novca (stanje: {Stanje}).");
        Stanje -= iznos;
    }
}

class Program
{
    static void Main()
    {
        var r = new Racun();
        r.Uplati(500);
        decimal[] zahtevi = { 200, -50, 1000, 300 };
        foreach (decimal z in zahtevi)
        {
            try
            {
                r.Isplati(z);
                Console.WriteLine($"Isplaceno {z}, stanje {r.Stanje}");
            }
            catch (ArgumentException e)
            {
                Console.WriteLine("Odbijeno: " + e.Message);
            }
            catch (InvalidOperationException e)
            {
                Console.WriteLine("Odbijeno: " + e.Message);
            }
        }
    }
}

Sopstveni izuzetak

Sopstveni izuzetak je klasa koja nasleđuje klasu Exception. Ime joj se po dogovoru završava sa Exception, a poruka se prosleđuje konstruktoru bazne klase sa : base(poruka). Isti postupak koristi i napredni primer sa putnicima u vozu.

using System;
using System.Collections.Generic;

class PrekoracenKapacitetException : Exception
{
    public int Kapacitet { get; }

    public PrekoracenKapacitetException(int kapacitet)
        : base($"Prekoracen kapacitet od {kapacitet} mesta.")
    {
        Kapacitet = kapacitet;
    }
}

class Autobus
{
    private readonly List<string> putnici = new List<string>();
    private readonly int kapacitet;

    public Autobus(int kapacitet) { this.kapacitet = kapacitet; }

    public int Broj => putnici.Count;

    public void Udji(string ime)
    {
        if (putnici.Count == kapacitet)
            throw new PrekoracenKapacitetException(kapacitet);
        putnici.Add(ime);
    }
}

class Program
{
    static void Main()
    {
        var bus = new Autobus(3);
        string[] red = { "Ana", "Bojan", "Ceca", "Dule", "Ema" };
        try
        {
            foreach (string ime in red)
            {
                bus.Udji(ime);
                Console.WriteLine($"{ime} je usao/la.");
            }
        }
        catch (PrekoracenKapacitetException e)
        {
            Console.WriteLine(e.Message);
        }
        finally
        {
            Console.WriteLine($"U autobusu: {bus.Broj}");
        }
    }
}

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje. Rešenja su provereno tačna na primerima iz teksta.

ZADATAK 01Težina: lako

Zbir ispravnih brojeva

U redu su reči razdvojene razmakom. Sabrati sve reči koje su celi brojevi i prebrojati one koje nisu.

Primer

Ulaz

5 abc 12 7.5 -3 x

Izlaz

Zbir: 14
Neispravnih: 3
ZADATAK 02Težina: srednje

Kalkulator

Svaki red ulaza je oblika a op b (celi brojevi i operacija + - * /), a ulaz se završava redom kraj. Za svaki red ispisati rezultat (deljenje je celobrojno) ili poruku Greska: neispravan broj, Greska: deljenje nulom ili Greska: nepoznata operacija X. Greške obraditi izuzecima.

Primer

Ulaz

8 + 5
7 / 0
9 ^ 2
12 * x
20 / 6
kraj

Izlaz

13
Greska: deljenje nulom
Greska: nepoznata operacija ^
Greska: neispravan broj
3
Česte greške i saveti
  • Hvataj samo izuzetke koje umeš da obradiš; prazan catch { } sakriva greške.
  • Konkretniji catch blokovi idu pre opštijih (catch (Exception) je poslednji).
  • Predvidive greške (unos) proveravaj unapred – TryParse je bolji od hvatanja FormatException.
  • Ime sopstvenog izuzetka završava se sa Exception, a klasa nasleđuje Exception.

Proveri znanje

Otvori kviz

Izuzeci u C#

1. Koji izuzetak baca int.Parse("abc")?




2. Kada se izvršava blok finally?




3. Kojom naredbom metoda sama prijavljuje grešku?




4. Koju klasu nasleđuje sopstveni izuzetak?