JavaRush /Курси /C# SELF /Стек викликів і створення власних винятків

Стек викликів і створення власних винятків

C# SELF
Рівень 13 , Лекція 3
Відкрита

1. Знайомство зі стеком викликів (Call Stack)

На попередній лекції ми коротко згадали стек викликів. Тепер розберемося з ним докладніше.

Уявіть: користувач натискає кнопку в застосунку. Кнопка викликає метод OnClick() («Кнопка натиснута»). Той, своєю чергою, викликає LoadData() (завантаження даних), а той — ReadFromFile() (читання з файла).

Раптом у ReadFromFile() стається помилка — файл не знайдено. Хто винен?

Щоб розібратися, програма йде назад слідами: від ReadFromFile() → до LoadData() → до OnClick(). Цей шлях і називають стеком викликів — як стос тарілок: першою падає та, що зверху.

Програма рухається цим стосом униз, доки не знайде відповідний catch, водночас виконує всі finally, що трапляються на шляху, — щоб усе було закрито, вивільнено й прибрано.

У програмуванні стек викликів — це список, що «памʼятає», хто кого викликав, аби за потреби пройти цей «ланцюжок викликів» у зворотному напрямку й знайти причину збою.

Як це влаштовано

Коли виконується програма, кожен виклик методу (або функції) додає новий «рядок» у стек (список) викликів. Якщо в процесі роботи виникає виняток, клас .NET Exception зберігає інформацію про цей стек: де і в якій послідовності викликалися методи, які призвели до помилки.

2. Навіщо взагалі потрібен стек викликів

Стек викликів — ваш найкращий помічник під час налагодження складних помилок.

  • Він показує не лише, що сталося, а й де саме та хто «винен».
  • Іноді, дивлячись на стек, ви дивуєтеся, як узагалі програма дійшла до такого стану (особливо якщо хтось випадково передав не те значення аргументу).

Типова історія: припустімо, у вас величезний проєкт, де методи викликають один одного на десяток рівнів углиб. У якийсь момент зʼявляється NullReferenceException, а ви не розумієте, як програма туди потрапила. Відкриваєте стек — бачите весь ланцюжок викликів, і вже набагато простіше зрозуміти, з чого почати розбиратися.

Приклад:

class MyClass 
{
    public void MethodA()     { MethodB(); }
    
    public void MethodB()     { MethodC(); }
    
    public void MethodC()     {
        throw new Exception("Помилка!");
    }    

    public void Main() 
    {
        try 
        { 
            MethodA(); 
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }        
    }
}

3. Як створювати власні винятки

Іноді стандартних винятків не вистачає

У .NET є чимало стандартних винятків (ArgumentNullException, InvalidOperationException тощо), але інколи цього недостатньо:

  • У вашому застосунку свої «правила гри»: наприклад, користувач не може купити більше 10 товарів за раз, або у вашій бізнес-логіці не має бути відʼємних сум.
  • Ви хочете відокремлювати помилки логіки застосунку від «системних» помилок.

Тут і виникає бажання створити власний тип — адже це можливо.


using System;

// Власний виняток: користувача не знайдено
public class UserNotFoundException : Exception
{
    // Базовий конструктор
    public UserNotFoundException() : base("Користувача не знайдено.") { }

    // Конструктор з повідомленням
    public UserNotFoundException(string message) : base(message) { }

    // Конструктор з повідомленням і вкладеним винятком
    public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
Приклад власного винятку UserNotFoundException

Коротко про конструктори

  • Без параметрів — встановлює стандартне повідомлення
  • З власним повідомленням — інколи потрібно додати деталі
  • З вкладеним винятком (inner) — щоб, якщо помилка сталася «всередині іншої помилки», не втратити важливу інформацію

Як використовувати свій виняток

Припустімо, моделюємо метод пошуку користувача в нашому застосунку для керування задачами:

using System;

public class UserService
{
    public string FindUserNameById(int userId)
    {
        // «Шукаємо» користувача, якщо не знайдено — викидаємо виняток
        if (userId != 42)
            throw new UserNotFoundException($"Користувача з ID {userId} не знайдено.");

        return "Максим";
    }
}

В основній програмі:

// В Main
var service = new UserService();
try
{
    string name = service.FindUserNameById(17);
    Console.WriteLine("Імʼя користувача: " + name);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine("Проблема пошуку користувача: " + ex.Message);
    // Стек викликів тут теж доступний через ex.StackTrace
}

Якщо передаємо ID, відмінний від 42, отримаємо:

Проблема пошуку користувача: Користувача з ID 17 не знайдено.

4. Причини створювати свої винятки

Журналювання та розмежування помилок

Припустімо, у вашому застосунку багато різних помилок, і їх треба обробляти по-різному. Наприклад, помилки бази даних журналювати як «фатальні», користувацькі — показувати користувачеві червоним повідомленням, а помилки мережі — намагатися повторити операцію. Групування за типом винятку — дієвий спосіб це реалізувати.

ООП і наслідування

Можна побудувати ієрархію помилок для своєї предметної області:

public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }

Тепер, перехопивши MyAppException, ви обробите усі помилки своєї предметної області, а якщо потрібна особлива реакція на «занадто велике замовлення» — перехоплюйте вже найконкретніший випадок.

5. Що варто памʼятати під час створення власних винятків

  • Не викидати винятки заради самих винятків
    Створюйте власні винятки лише тоді, коли:
    • Це справді робить код зрозумілішим
    • Є шанс, що їх перехоплюватимуть у викличному коді
    • Потрібно передати більше інформації викличному коду (через поля або властивості)
  • Добра практика: серіалізація
    У .NET стандартні винятки підтримують серіалізацію (наприклад, для передавання мережею). У простих застосунках серіалізація рідко потрібна, але для складніших випадків — додайте атрибут [Serializable] і реалізуйте конструктор для серіалізації (див. офіційну документацію). Для навчальних прикладів це не обовʼязково, а на роботі — уточніть у лідера команди.
    Серіалізація — це процес перетворення обʼєкта у формат, зручний для зберігання або передавання (наприклад, у файл чи мережею). Ми детальніше розглянемо серіалізацію пізніше.

6. Особливості стеку викликів: де може виникнути плутанина

Стек викликів показує лише шлях до тієї точки, де виник виняток. Якщо ви перехоплюєте виняток і викидаєте новий без зазначення «вкладеного» винятку (про конструктор Exception(string, Exception inner)), можете втратити першоджерело помилки. Це називають «приховуванням стеку».

Погано:

try
{
    // Тут якась помилка
}
catch (Exception ex)
{
    throw new Exception("Сталася невідома помилка."); // Стек минулої помилки втрачається!
}

Добре:

try
{
    // Тут якась помилка
}
catch (Exception ex)
{
    throw new Exception("Сталася невідома помилка.", ex); // Зберігаємо оригінальний стек!
}

Тоді в StackTrace залишиться і шлях до першої помилки, і ваше нове повідомлення.

7. Практичні поради та часті помилки

  • Не варто викидати винятки для керування звичайною логікою (наприклад, для «виходу з циклу» — є елегантніші способи).
  • Перехоплюйте потрібний тип винятку — завжди перехоплювати Exception — не найкраща ідея (можете «проковтнути» помилку, яку не слід було б ігнорувати).
  • Завжди журналюйте стек викликів у разі складних помилок. Це збереже купу нервів під час пошуку багів.
  • Використовуйте вкладені винятки — це допомагає не втрачати інформацію про першопричину збою.
  • Описуйте свої винятки зрозуміло — щоб той, хто читатиме код за рік, одразу зрозумів, чому сталася ця помилка.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ