Модул 11

Изключения и обработка на грешки (try-catch)

Какво се случва, когато нещо се обърка по време на изпълнение — какво е изключение, как се хваща с try-catch, кога се използва finally, как сами хвърляме грешки с throw и кои са най-честите изключения.

Теория

Изключението (exception) е сигнал, че по време на изпълнение се е случило нещо, с което кодът не може да продължи нормално: текст вместо число, индекс извън масива, делене на нула, липсващ файл. Ако никой не "хване" изключението, програмата се срива със съобщение за грешка.

Конструкцията try-catch ни позволява да реагираме: в try блока поставяме "рисковия" код, а в catch — какво да се направи, ако той гръмне. Изпълнението на try спира на реда с грешката и скача директно в catch; след това програмата продължава нормално.

Можем да хващаме конкретни типове изключения с отделни catch блокове (catch (FormatException ex)) — от по-конкретния към по-общия. Обектът ex носи информация: ex.Message е човешкото описание на проблема. Блокът finally се изпълнява винаги — и при успех, и при грешка — и служи за почистване (затваряне на файлове, връзки).

С throw ние сами хвърляме изключение, когато нашият метод получи невалидни данни: throw new ArgumentException("Възрастта не може да е отрицателна!");. Но помнете: изключенията са за изключителни ситуации. Ако грешката е очаквана (потребителят пише глупости), по-добре я предотвратете с проверка или TryParse, вместо да я хващате.

Изключението е обект с полезна информация: ex.Message е човешкото описание, а ex.StackTrace — пътят на извикванията до мястото на грешката (последното е безценно при дебъгване). Нехванатото изключение се "качва" нагоре по веригата от извиквания — от метода към този, който го е извикал, и така до Main; ако и там никой не го хване, програмата се срива. Това качване се нарича разпространение (bubbling) и означава, че catch-ът не е длъжен да е в същия метод, където е грешката.

Златни правила

try пази, catch реагира

Рисковият код отива в try, реакцията при грешка — в catch. Стигне ли се до грешка, останалата част от try блока се прескача.

От конкретното към общото

При няколко catch блока подреждайте от най-конкретния тип към най-общия (Exception е последен) — изпълнява се първият съвпадащ.

finally се изпълнява винаги

Кодът във finally блока се изпълнява и при успех, и при грешка — там се затварят файлове и се освобождават ресурси.

Не хващайте всичко наред

Празен catch { }, който "глътва" грешката мълчаливо, е лоша практика — грешката изчезва и никой не разбира какво се е объркало. Хванете, съобщете, реагирайте.

Бърз справочник

Най-честите изключения

ИзключениеКога възниква?Как се предотвратява?
FormatExceptionint.Parse("абв") — текстът не е числоint.TryParse() вместо Parse
NullReferenceExceptionДостъп до член на обект, който е nullПроверка if (obj != null) преди употреба
IndexOutOfRangeExceptionИндекс извън границите на масивИндекси само от 0 до Length - 1
DivideByZeroExceptionЦелочислено делене на нулаПроверка if (b != 0) преди делене
KeyNotFoundExceptionЧетене на липсващ ключ от речникContainsKey() или TryGetValue()
FileNotFoundExceptionЧетене на несъществуващ файлFile.Exists() преди четене

Структура на try-catch-finally

БлокКога се изпълнява?За какво служи?
try { }Винаги (до първата грешка)Рисковият код
catch (FormatException ex) { }Само при този тип грешка в tryКонкретна реакция
catch (Exception ex) { }При всяка (друга) грешка в tryОбща "предпазна мрежа", винаги последен
finally { }Винаги — и при успех, и при грешкаПочистване (файлове, ресурси)
throw new Exception("...")Когато ние решимСами сигнализираме за невалидни данни

Код за анализ и пренаписване

Безопасно делене с try-catch-finally

Program.cs
using System;

namespace ModuleEleven
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.Write("Въведи делимо: ");
                int a = int.Parse(Console.ReadLine());

                Console.Write("Въведи делител: ");
                int b = int.Parse(Console.ReadLine());

                int result = a / b;
                Console.WriteLine($"Резултат: {result}");
            }
            catch (FormatException)
            {
                Console.WriteLine("Грешка: Това не е валидно число!");
            }
            catch (DivideByZeroException)
            {
                Console.WriteLine("Грешка: Не може да се дели на нула!");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Неочаквана грешка: {ex.Message}");
            }
            finally
            {
                Console.WriteLine("Програмата приключи. (Този ред се печата ВИНАГИ.)");
            }
        }
    }
}

Какво се случва тук?

  • Целият "рисков" код — двете парсвания и деленето — е в try блока. Гръмне ли някой ред, останалата част от try се прескача и управлението скача в съвпадащия catch.
  • Въведе ли потребителят "абв", int.Parse хвърля FormatException → изпълнява се първият catch и програмата НЕ се срива.
  • Въведе ли 0 за делител, a / b хвърля DivideByZeroException → вторият catch. Забележете: всеки тип грешка си има собствено, смислено съобщение.
  • Третият catch (Exception ex) е "предпазната мрежа" за всичко неочаквано — той е най-общият тип и затова стои последен. ex.Message съдържа описанието на грешката.
  • finally се изпълнява при всички сценарии — успех, хванат FormatException, каквото и да е. Тук е мястото за почистване на ресурси.
  • Алтернатива без изключения: int.TryParse за входа и проверка if (b == 0) — предотвратяването винаги е за предпочитане, когато грешката е очаквана.

throw: класът сам пази правилата си

Program.cs
using System;

namespace ModuleElevenExtra
{
    class BankAccount
    {
        private double balance;

        public double Balance
        {
            get { return balance; }
        }

        public void Deposit(double amount)
        {
            if (amount <= 0)
            {
                throw new ArgumentException("Сумата трябва да е положителна!");
            }
            balance += amount;
        }

        public void Withdraw(double amount)
        {
            if (amount > balance)
            {
                throw new InvalidOperationException("Недостатъчна наличност!");
            }
            balance -= amount;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            BankAccount account = new BankAccount();

            try
            {
                account.Deposit(100);
                account.Withdraw(250); // тук ще гръмне
                Console.WriteLine("Тегленето мина."); // не се стига дотук
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"Невалидна сума: {ex.Message}");
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine($"Отказана операция: {ex.Message}");
            }

            Console.WriteLine($"Баланс: {account.Balance} лв.");
        }
    }
}

Какво се случва тук?

  • Тук изключенията пътуват между методи: Withdraw хвърля, а хваща Main — изключението се "качва" към извикващия (bubbling). Класът само сигнализира; как ще се реагира, решава ползващият го код.
  • throw new InvalidOperationException("...") създава обект-изключение с наше съобщение — точно то излиза после от ex.Message.
  • Изборът на тип е смислов: ArgumentException = "подаде ми невалидна стойност", InvalidOperationException = "операцията не е позволена в това състояние". Стандартните типове правят кода разбираем за всеки C# програмист.
  • Така комбинирани, инкапсулацията (Модул 6) и изключенията гарантират, че обектът никога не влиза в невалидно състояние — балансът не може да стане отрицателен, колкото и да греши ползващият код.
  • Забележете: редът след Withdraw(250) не се изпълнява — изпълнението скача директно в catch, а след него животът продължава с печата на баланса.

Внимавай! Чести грешки

Грешките, които почти всеки прави в тази тема — виж разликата между грешния и правилния код.

Празен catch — "глътната" грешка

Грешно

try
{
    int n = int.Parse(input);
}
catch { }
// грешката изчезва безследно

Правилно

try
{
    int n = int.Parse(input);
}
catch (FormatException)
{
    Console.WriteLine("Това не е число! Опитай пак.");
}

Празният catch скрива проблема: програмата продължава с невалидни данни и гърми по-късно някъде далеч от истинската причина. Хванете конкретния тип и реагирайте видимо.

Общият catch преди конкретните

Грешно

catch (Exception ex)
{
    Console.WriteLine("Някаква грешка");
}
catch (FormatException ex) // CS0160: недостижим
{
    ...
}

Правилно

catch (FormatException ex)
{
    Console.WriteLine("Невалидно число!");
}
catch (Exception ex)
{
    Console.WriteLine($"Неочаквана грешка: {ex.Message}");
}

Exception съвпада с всичко — сложен пръв, той поглъща всички грешки и конкретните блокове стават недостижими (компилаторът дава CS0160). Винаги: от конкретното към общото.

Изключения вместо проверка за очакван вход

Грешно

try
{
    int n = int.Parse(Console.ReadLine());
}
catch (FormatException)
{
    // "нормалният" път минава през грешка

Правилно

int n;
while (!int.TryParse(Console.ReadLine(), out n))
{
    Console.Write("Невалидно число, опитай пак: ");
}

Грешният потребителски вход е очакван, не изключителен — за него има TryParse. Изключенията оставете за наистина неочакваното. По-чисто, по-четимо и по-бързо.

Задачи за самостоятелна работа

Опитай първо сам — подсказката е там само ако наистина закъсаш.

Задача 1: Неуморният вход

Напишете програма, която пита за число и НЕ се отказва: при невалиден вход хваща грешката и пита отново, докато потребителят не въведе валидно число. Решете я веднъж с try-catch и веднъж с int.TryParse.

Задача 2: Безопасен банкомат

Разширете класа BankAccount от Модул 6: методът Withdraw да хвърля ArgumentException при отрицателна сума и InvalidOperationException при недостатъчна наличност. В Main хванете двете грешки с отделни catch блокове.

Задача 3: Метод ReadAge с throw

Напишете метод ReadAge(), който чете възраст от конзолата и хвърля ArgumentException, ако тя е под 0 или над 120. В Main извиквайте метода в цикъл с try-catch, докато не получите валидна възраст.

Мини-тест за проверка

Отговори си наум (или на глас!) и чак тогава разкрий отговора.

1. Какво се случва с кода в try блока след реда, на който е възникнала грешка?

2. Кога се изпълнява finally блокът?

3. Защо catch (Exception ex) трябва да е последният catch блок?

4. Какво представлява обектът на изключението (ex) и какво носи в себе си?

Речник на термините

Виж пълния речник на курса
Изключение (Exception)
Обект-сигнал, че изпълнението не може да продължи нормално. Нехванато — срива програмата.
try / catch
try огражда рисковия код; catch поема управлението при грешка от съответния тип.
finally
Блок, който се изпълнява винаги — при успех и при грешка. Място за почистване на ресурси.
throw
Хвърляне на изключение от наш код: throw new ArgumentException("..."); — начинът класът да защити правилата си.
ex.Message
Човешкото описание на грешката — текстът, който показваме или записваме в лог.
Stack trace
Списъкът от методи до мястото на грешката, с редове — картата за намиране на проблема.
Разпространение (bubbling)
Нехванатото изключение се "качва" от метод към извикващия го, докато някой не го хване (или програмата не се срине).
Защитно програмиране
Предотвратяване вместо лекуване: проверки на входа, TryParse, File.Exists — изключенията остават за неочакваното.

Провери знанията

Бърз тест с избор от отговори върху термините на модула — с точки и моментална обратна връзка.

Провери знанията си

8 въпроса върху термините от модула. Показваме определение — ти избираш правилния термин.

Препоръки за този модул

  • Първо се научете да ЧЕТЕТЕ съобщението за грешка: типът (FormatException) казва какво се е объркало, а stack trace-ът — на кой ред.
  • Предизвиквайте грешки нарочно: въведете "абв" вместо число, разделете на нула, излезте от масива — после ги хванете с try-catch.
  • Предпочитайте предотвратяване пред хващане: TryParse вместо Parse + catch, проверка File.Exists преди четене, проверка на делителя преди делене.
  • Отпечатвайте ex.Message в catch блока, докато се учите — така виждате какво точно сте хванали.

Начален курс по C# — учебен наръчник и бърз справочник за студенти.

Натисни Ctrl K или / отвсякъде, за да търсиш.