$ sudo teach IT
Модуль 7 · Обработка данных

Урок 7.2 — Конструкция try-catch-finally

Обработка исключений: программа не должна падать. Перехват, проброс и очистка ресурсов.

Представь, что ты едешь на машине и внезапно лопается колесо. Что делает опытный водитель? Он обрабатывает ситуацию: достаёт запаску, меняет колесо и едет дальше. Новичок же просто бросает машину на дороге. В программировании то же самое: ошибки будут возникать всегда. Важно не игнорировать их, а уметь правильно перехватывать и реагировать. Конструкция try-catch-finally — это твоя «запаска».

❓

Почему программы падают?

Ошибки в программе делятся на три категории:

Тип
Описание
Пример
Синтаксическая
Ошибка в написании кода
int x = "текст";
Логическая
Код работает, но не так
return a - b; вместо a + b
Исключение (runtime)
Внезапная ошибка во время выполнения
Деление на 0, файл не найден

Синтаксические ошибки ловит компилятор — программа даже не запустится. Логические — твоя ответственность с помощью тестов. А вот исключения (exceptions) возникают уже во время работы программы, и их нужно обрабатывать с помощью try-catch.

C# · Пример исключения
int a = 10;
int b = 0;
int result = a / b;  // ❌ DivideByZeroException!
Console.WriteLine(result);  // сюда уже не дойдём
📝

Синтаксис try-catch

Базовая конструкция состоит из блока try (код, который может упасть) и catch (код-обработчик):

try { // опасный код }
catch (ТипИсключения переменная) { // обработка }

C# · Простейший 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 подряд:

C# · Несколько 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.Message
Текст ошибки для пользователя
ex.StackTrace
Цепочка вызовов, где произошла ошибка
ex.Source
Имя сборки, где возникло исключение
ex.InnerException
Вложенное исключение (если есть)
C# · Использование Exception ex
try
{
    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 выполняется всегда — и если ошибка была, и если её не было. Он используется для «уборки»: закрытие файлов, освобождение ресурсов, сброс состояния.

C# · 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() при выходе из блока:

C# · using
// Вместо 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. Это нужно, когда ты сам обнаружил некорректную ситуацию:

C# · 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; — сбрасывает стек (плохая практика).

C# · Проброс исключения
try
{
    // ... какой-то код ...
}
catch (Exception ex)
{
    // Логируем ошибку
    LogError(ex);

    // Пробрасываем дальше (правильно — с сохранением стека)
    throw;

    // ❌ Неправильно — теряет стек вызовов:
    // throw ex;
}
🏗️

Свои исключения (Custom Exceptions)

Можно создавать собственные типы исключений, наследуя от Exception. Это удобно, когда в твоей программе есть специфические ошибки:

C# · Своё исключение
// Создаём своё исключение
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. Вот основные классы, которые тебе нужно знать:

Тип исключения
Когда возникает
🚫
NullReferenceException
Обращение к null-объекту
📏
IndexOutOfRangeException
Выход за границы массива/списка
📝
ArgumentException
Некорректный аргумент метода
➗
DivideByZeroException
Деление целого числа на 0
📁
FileNotFoundException
Файл не найден

Полная иерархия выглядит так:

C# · Иерархия
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    // ваш тип
🧩

Полный пример: калькулятор с обработкой ошибок

Давай соберём всё вместе — напишем калькулятор, который обрабатывает все возможные ошибки:

C# · Калькулятор с try-catch
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.

🧩

Задание для тренировки

Напиши программу «Банкомат», которая:

  1. Создаёт класс BankAccount с балансом
  2. Реализует методы Deposit и Withdraw
  3. Создаёт своё исключение TransactionException
  4. Выбрасывает исключения при: отрицательной сумме, превышении лимита (сумма > 100000), недостатке средств
  5. Основная программа в цикле предлагает операции и корректно обрабатывает все ошибки
  6. Использует finally для логирования каждой операции
C# · Решение
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("--- Операция завершена ---");
            }
        }
    }
}
📌

Что важно запомнить

1️⃣
try — опасный код, catch — обработка, finally — уборка. Конструкция try-catch-finally — стандартный паттерн обработки исключений в C#. finally выполняется всегда.
2️⃣
Указывай конкретные типы исключений. Не лови все ошибки одним catch (Exception) без необходимости. Используй несколько catch — от частного к общему.
3️⃣
throw — бросай исключения сам. Если метод не может выполнить свою работу — не возвращай код ошибки, а бросай исключение. Создавай свои типы исключений наследованием от Exception.
4️⃣
using — лучший друг ресурсов. Для файлов, соединений и других IDisposable используй using. Он гарантирует освобождение даже при исключении.
5️⃣
Иерархия исключений: все исключения наследуют от Exception. NullReferenceException, DivideByZeroException, FileNotFoundException, ArgumentException — самые частые. Знай их в лицо.

Тест: 7.2: Конструкция try-catch-finally

3 вопроса

Обработка данных

Premium