Урок 7.2 — Конструкция try-catch-finally
Обработка исключений: программа не должна падать. Перехват, проброс и очистка ресурсов.
Представь, что ты едешь на машине и внезапно лопается колесо. Что делает опытный водитель? Он обрабатывает ситуацию: достаёт запаску, меняет колесо и едет дальше. Новичок же просто бросает машину на дороге. В программировании то же самое: ошибки будут возникать всегда. Важно не игнорировать их, а уметь правильно перехватывать и реагировать. Конструкция try-catch-finally — это твоя «запаска».
Почему программы падают?
Ошибки в программе делятся на три категории:
int x = "текст";return a - b; вместо a + bСинтаксические ошибки ловит компилятор — программа даже не запустится. Логические — твоя ответственность с помощью тестов. А вот исключения (exceptions) возникают уже во время работы программы, и их нужно обрабатывать с помощью try-catch.
int a = 10;
int b = 0;
int result = a / b; // ❌ DivideByZeroException!
Console.WriteLine(result); // сюда уже не дойдём
Синтаксис try-catch
Базовая конструкция состоит из блока try (код, который может упасть) и catch (код-обработчик):
try { // опасный код }
catch (ТипИсключения переменная) { // обработка }
using System;
class Program
{
static void Main()
{
try
{
Console.Write("Введи число: ");
int number = int.Parse(Console.ReadLine());
Console.WriteLine($"Ты ввёл: {number}");
}
catch
{
Console.WriteLine("Ошибка! Это не число.");
}
Console.WriteLine("Программа продолжается...");
}
}
Если пользователь введёт «abc», int.Parse выбросит исключение FormatException, и выполнение перейдёт в catch. Программа не упадёт — она сообщит об ошибке и продолжит работу.
catch не указан тип исключения, он перехватывает все исключения. Это удобно для быстрой защиты, но на практике лучше указывать конкретные типы.Конкретные исключения и несколько catch
Ты можешь указать конкретный тип исключения и обрабатывать разные ошибки по-разному. Можно использовать несколько блоков catch подряд:
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 (DivideByZeroException)
{
Console.WriteLine("На ноль делить нельзя!");
}
catch (FormatException)
{
Console.WriteLine("Нужно вводить целые числа!");
}
catch (OverflowException)
{
Console.WriteLine("Число слишком большое или маленькое!");
}
catch (Exception ex)
{
Console.WriteLine($"Неизвестная ошибка: {ex.Message}");
}
⚠️ Важно: блоки catch проверяются сверху вниз, и выполнится первый подходящий. Поэтому сначала пиши более конкретные исключения (DivideByZeroException), а потом более общие (Exception). Если поставить catch (Exception) первым — все остальные блоки никогда не сработают!
Переменная ex в catch (Exception ex) содержит информацию об ошибке:
ex.Messageex.StackTraceex.Sourceex.InnerExceptiontry
{
string text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Файл не найден: {ex.FileName}");
Console.WriteLine($"Полный путь: {ex.FileName}");
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка: {ex.Message}");
// Для отладки:
Console.WriteLine($"Стек: {ex.StackTrace}");
}
Блок finally
Блок finally выполняется всегда — и если ошибка была, и если её не было. Он используется для «уборки»: закрытие файлов, освобождение ресурсов, сброс состояния.
FileStream file = null;
try
{
file = File.OpenRead("data.txt");
// ... работа с файлом ...
Console.WriteLine("Файл открыт и обработан");
}
catch (FileNotFoundException)
{
Console.WriteLine("Файл не найден!");
}
finally
{
if (file != null)
{
file.Close(); // обязательно закрываем файл
Console.WriteLine("Файл закрыт в finally");
}
}
// finally выполнится в любом из этих сценариев:
// 1. try выполнился успешно → finally
// 2. catch перехватил ошибку → finally
// 3. Исключение не было перехвачено → finally (исключение летит дальше)
✅ finally vs код после try-catch: код после try-catch выполняется, только если исключение было перехвачено. Finally выполняется всегда, даже если исключение не перехвачено (и программа завершится).
using — элегантная замена finally для ресурсов
Для типов, реализующих IDisposable (файлы, соединения с БД, сетевые сокеты), есть конструкция using, которая автоматически вызывает Dispose() при выходе из блока:
// Вместо try-finally с Close():
using (FileStream file = File.OpenRead("data.txt"))
{
// ... работа с файлом ...
// file.Dispose() вызовется автоматически при выходе
}
// Сокращённый синтаксис (C# 8+):
using var reader = new StreamReader("data.txt");
string content = reader.ReadToEnd();
// reader.Dispose() вызовется в конце метода
// using работает с любым IDisposable
using var connection = new SqlConnection("...");
using var writer = new StreamWriter("log.txt");
using — самый безопасный способ работы с ресурсами. Она гарантирует освобождение даже при исключении. В C# 8+ можно писать using var без фигурных скобок — ресурс освободится в конце текущего блока/метода.throw — выброс исключения
Ты можешь не только ловить, но и бросать исключения с помощью throw. Это нужно, когда ты сам обнаружил некорректную ситуацию:
static void SetAge(int age)
{
if (age < 0)
{
throw new ArgumentException("Возраст не может быть отрицательным!");
}
if (age > 150)
{
throw new ArgumentOutOfRangeException(nameof(age),
"Возраст не может быть больше 150 лет");
}
Console.WriteLine($"Возраст: {age}");
}
// Попробуем:
try
{
SetAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Ошибка: {ex.Message}");
}
⚠️ Важно: throw может использоваться также внутри catch для «проброса» исключения дальше: throw; — сохраняет оригинальный стек вызовов. throw ex; — сбрасывает стек (плохая практика).
try
{
// ... какой-то код ...
}
catch (Exception ex)
{
// Логируем ошибку
LogError(ex);
// Пробрасываем дальше (правильно — с сохранением стека)
throw;
// ❌ Неправильно — теряет стек вызовов:
// throw ex;
}
Свои исключения (Custom Exceptions)
Можно создавать собственные типы исключений, наследуя от Exception. Это удобно, когда в твоей программе есть специфические ошибки:
// Создаём своё исключение
class InsufficientFundsException : Exception
{
// Можно добавить свои свойства
public decimal Balance { get; }
public decimal Amount { get; }
public InsufficientFundsException(decimal balance, decimal amount)
: base($"Недостаточно средств. Баланс: {balance}, требуется: {amount}")
{
Balance = balance;
Amount = amount;
}
// Рекомендуется реализовать 3 стандартных конструктора
public InsufficientFundsException() { }
public InsufficientFundsException(string message) : base(message) { }
public InsufficientFundsException(string message, Exception inner)
: base(message, inner) { }
}
// Использование:
class BankAccount
{
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Сумма должна быть положительной");
if (amount > Balance)
throw new InsufficientFundsException(Balance, amount);
Balance -= amount;
Console.WriteLine($"Снято {amount}. Остаток: {Balance}");
}
}
// Обработка:
try
{
var account = new BankAccount();
account.Withdraw(1000);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Ошибка: {ex.Message}");
Console.WriteLine($"Баланс: {ex.Balance}, нужно: {ex.Amount}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Некорректный запрос: {ex.Message}");
}
Exception (InsufficientFundsException). Наследуй от Exception (или ApplicationException). Добавь хотя бы 3 конструктора: без параметров, с message, с message и inner exception.Иерархия исключений в C#
Все исключения в C# наследуются от одного предка — System.Exception. Вот основные классы, которые тебе нужно знать:
NullReferenceExceptionIndexOutOfRangeExceptionArgumentExceptionDivideByZeroExceptionFileNotFoundExceptionПолная иерархия выглядит так:
System.Exception
├── System.SystemException
│ ├── NullReferenceException // object == null
│ ├── IndexOutOfRangeException // arr[100] при длине 5
│ ├── DivideByZeroException // int / 0
│ ├── OutOfMemoryException // не хватило памяти
│ ├── InvalidCastException // приведение типов
│ └── StackOverflowException // переполнение стека
├── System.ArgumentException
│ ├── ArgumentNullException // null вместо аргумента
│ └── ArgumentOutOfRangeException // аргумент вне диапазона
├── System.IO.IOException
│ ├── FileNotFoundException // файл не существует
│ ├── DirectoryNotFoundException // каталог не существует
│ └── DriveNotFoundException // диска нет
├── System.FormatException // неверный формат (int.Parse)
├── System.InvalidOperationException // операция невозможна
└── ВашеСвоёИсключение : Exception // ваш тип
Полный пример: калькулятор с обработкой ошибок
Давай соберём всё вместе — напишем калькулятор, который обрабатывает все возможные ошибки:
using System;
class Program
{
static void Main()
{
while (true)
{
try
{
Console.WriteLine("\n=== КАЛЬКУЛЯТОР ===");
Console.Write("Введи первое число: ");
double a = double.Parse(Console.ReadLine());
Console.Write("Введи операцию (+, -, *, /): ");
string op = Console.ReadLine();
Console.Write("Введи второе число: ");
double b = double.Parse(Console.ReadLine());
double result = op switch
{
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" when b != 0 => a / b,
"/" => throw new DivideByZeroException(),
_ => throw new InvalidOperationException(
$"Неизвестная операция: {op}")
};
Console.WriteLine($"Результат: {a} {op} {b} = {result}");
}
catch (FormatException)
{
Console.WriteLine("❌ Ошибка: введи число, а не текст!");
}
catch (DivideByZeroException)
{
Console.WriteLine("❌ Ошибка: на ноль делить нельзя!");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"❌ {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Неожиданная ошибка: {ex.Message}");
}
finally
{
Console.WriteLine("--- Итерация завершена ---");
}
Console.Write("\nЕщё раз? (y/n): ");
if (Console.ReadLine()?.ToLower() != "y")
break;
}
Console.WriteLine("Пока!");
}
}
Мини-тест
Вопрос 1
Что выведет код, если пользователь введёт «abc»? try { int.Parse(Console.ReadLine()); } catch { Console.Write("E"); } finally { Console.Write("F"); }
Ответ: EF. Сначала сработает catch (вывод E), затем finally (вывод F).
Вопрос 2
Какой порядок блоков catch правильный?
Ответ: сначала конкретные исключения (DivideByZeroException), потом общие (Exception). Иначе более общий catch «перехватит» все исключения.
Вопрос 3
Что делает throw; внутри catch?
Ответ: пробрасывает текущее исключение дальше, сохраняя оригинальный стек вызовов. throw ex; — плохая практика, теряет стек.
Вопрос 4
Какой блок выполняется всегда — и при ошибке, и без ошибки?
Ответ: finally. Он используется для освобождения ресурсов. Альтернатива: using для IDisposable.
Задание для тренировки
Напиши программу «Банкомат», которая:
- Создаёт класс
BankAccountс балансом - Реализует методы
DepositиWithdraw - Создаёт своё исключение
TransactionException - Выбрасывает исключения при: отрицательной сумме, превышении лимита (сумма > 100000), недостатке средств
- Основная программа в цикле предлагает операции и корректно обрабатывает все ошибки
- Использует finally для логирования каждой операции
using System;
class TransactionException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }
public TransactionException(string msg, decimal balance, decimal amount)
: base(msg) { Balance = balance; Amount = amount; }
}
class BankAccount
{
public decimal Balance { get; private set; }
private const decimal Limit = 100000;
public BankAccount(decimal initial)
{
if (initial < 0)
throw new TransactionException(
"Начальный баланс не может быть отрицательным", 0, initial);
Balance = initial;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new TransactionException(
"Сумма должна быть положительной", Balance, amount);
if (amount > Limit)
throw new TransactionException(
$"Превышен лимит {Limit}", Balance, amount);
Balance += amount;
Console.WriteLine($"✅ Внесено {amount}. Баланс: {Balance}");
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new TransactionException(
"Сумма должна быть положительной", Balance, amount);
if (amount > Balance)
throw new TransactionException(
"Недостаточно средств", Balance, amount);
Balance -= amount;
Console.WriteLine($"✅ Снято {amount}. Баланс: {Balance}");
}
}
class Program
{
static void Main()
{
var account = new BankAccount(5000);
while (true)
{
try
{
Console.WriteLine($"\n=== БАНКОМАТ === Баланс: {account.Balance}");
Console.WriteLine("1. Внести 2. Снять 3. Выход");
Console.Write("Выбор: ");
var choice = Console.ReadLine();
if (choice == "3") break;
Console.Write("Сумма: ");
decimal amount = decimal.Parse(Console.ReadLine());
if (choice == "1")
account.Deposit(amount);
else if (choice == "2")
account.Withdraw(amount);
else
Console.WriteLine("Неверный выбор");
}
catch (TransactionException ex)
{
Console.WriteLine($"❌ {ex.Message}");
}
catch (FormatException)
{
Console.WriteLine("❌ Введи число!");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Ошибка: {ex.Message}");
}
finally
{
Console.WriteLine("--- Операция завершена ---");
}
}
}
}
Что важно запомнить
try-catch-finally — стандартный паттерн обработки исключений в C#. finally выполняется всегда.catch (Exception) без необходимости. Используй несколько catch — от частного к общему.Exception.IDisposable используй using. Он гарантирует освобождение даже при исключении.Exception. NullReferenceException, DivideByZeroException, FileNotFoundException, ArgumentException — самые частые. Знай их в лицо.Тест: 7.2: Конструкция try-catch-finally
3 вопроса