Теория
Изключението (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 { }, който "глътва" грешката мълчаливо, е лоша практика — грешката изчезва и никой не разбира какво се е объркало. Хванете, съобщете, реагирайте.
Бърз справочник
Най-честите изключения
| Изключение | Кога възниква? | Как се предотвратява? |
|---|---|---|
FormatException | int.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
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: класът сам пази правилата си
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) и какво носи в себе си?
Речник на термините
Виж пълния речник на курсаtry огражда рисковия код; catch поема управлението при грешка от съответния тип.throw new ArgumentException("..."); — начинът класът да защити правилата си.Провери знанията
Бърз тест с избор от отговори върху термините на модула — с точки и моментална обратна връзка.
Провери знанията си
8 въпроса върху термините от модула. Показваме определение — ти избираш правилния термин.
Препоръки за този модул
- Първо се научете да ЧЕТЕТЕ съобщението за грешка: типът (
FormatException) казва какво се е объркало, а stack trace-ът — на кой ред. - Предизвиквайте грешки нарочно: въведете "абв" вместо число, разделете на нула, излезте от масива — после ги хванете с try-catch.
- Предпочитайте предотвратяване пред хващане:
TryParseвместоParse+ catch, проверкаFile.Existsпреди четене, проверка на делителя преди делене. - Отпечатвайте
ex.Messageв catch блока, докато се учите — така виждате какво точно сте хванали.