JavaRush /Курси /C# SELF /Закриття потоків і звільнення ресурсів (

Закриття потоків і звільнення ресурсів ( using)

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

1. Як правильно звільняти ресурси в C#?

Уявіть собі операційну систему як суворого бібліотекаря. Ви взяли книгу (відкрили файл/потік), читаєте, а потім… забули повернути! Бібліотекар обурюється: «Як це — книга досі у вас?!» Так само і з потоками. Відкритий потік займає ресурси: файловий дескриптор, шматок памʼяті, ще й блокує файл для інших застосунків.

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

У чому небезпека?

  • Файл не закривається, дані не потрапляють на диск (наприклад, під час запису — дані можуть залишитися в буфері).
  • Файл блокується для інших процесів — колеги й інші програми зляться.
  • Ліміт дескрипторів: у Windows/Linux у процесів є обмеження на кількість відкритих файлів/потоків.

Інтерфейс IDisposable

Будь-який клас, що працює з некерованими ресурсами (потоки, файли, бази даних, сокети), має реалізовувати інтерфейс IDisposable.


public interface IDisposable
{
    void Dispose();
}

У методі Dispose() зазвичай звільняють усі задіяні ресурси: файл нарешті закривається, зʼєднання розриваються, памʼять звільняється.

Потоки — це обʼєкти, які тримають у руках різні важливі ресурси: файли, зʼєднання, памʼять. Якщо їх не закрити, то файл може залишитися заблокованим (ваш Word скаже «файл відкрито іншим застосунком!»), а система — без памʼяті. Тому дуже важливо звільняти потік після завершення роботи.

У .NET потоки реалізують інтерфейс IDisposable. Це означає: їх слід закривати, викликаючи Dispose() (або просто огорнувши в блок using).

2. Варіанти закриття потоку: від небезпечного до надійного

Варіант 1. «Ручне» закриття: не робіть так!

Це застарілий підхід. І хоча в попередніх прикладах я його показував, у сучасній розробці так майже ніхто не робить.


var stream = new FileStream("file.txt", FileMode.Open);
// Працюємо з потоком
stream.Close(); // або stream.Dispose()

Проблема: якщо між відкриттям і закриттям станеться помилка або виняток, файл залишиться відкритим і заблокованим. Це ніби ви вибігли з бібліотеки разом із книгою, відчувши запах піци…

Варіант 2. Використовуємо try...finally


FileStream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.Open);
    // Працюємо з потоком
}
finally
{
    if (stream != null)
        stream.Dispose();
}

Такий варіант надійний: блок finally гарантовано виконається навіть у разі помилки. Але, зізнаймося, писати так незручно.

Варіант 3. Елегантно й безпечно: оператор using

Класичний синтаксис (using ( ... ) { ... })


using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // Працюємо з потоком
}
// Тут stream.Dispose() викличеться автоматично!

Головна ідея — усе, що всередині блоку using, працює з потоком, а коли блок завершується — файл закривається, навіть якщо щось пішло не так (наприклад, було кинуто виняток).

Варіант 4. Сучасний синтаксис

Сучасний синтаксис (using var)


using var stream = new FileStream("file.txt", FileMode.Open);
// Працюємо з потоком
// ... Dispose викличеться автоматично, коли змінна вийде з області видимості

Чудово! Не доведеться додавати зайві відступи й фігурні дужки.

Як це працює «під капотом»?

Оператор using компілятор розгортає в той самий try...finally — але вже за вас. Жарт: «using пише чистий код замість вас — може, він скоро почне пити каву й залипати на Stack Overflow?».

Різниця між класичним і сучасним using

Класичний using-блок using var (declaration)
Вигляд
using (var x = ...) { ... }
using var x = ...; ...
Область дії Всередині фігурних дужок блоку До кінця поточного блоку (методу, циклу тощо)
Стислість Трохи багатослівніше Лаконічно, менше відступів
Починаючи з C# 1.0 C# 8.0 і новіші

3. Що таке using-оголошення?

5 років тому світ побачив новий, лаконічний спосіб роботи з IDisposable-обʼєктами.

using-оголошення — це коли ви замість блоку оголошуєте змінну з ключовим словом using, і її буде автоматично звільнено наприкінці поточного блоку (наприклад, методу або циклу), а не в кінці фігурних дужок додаткового блоку.


using var stream = new FileStream("file.txt", FileMode.Open);
// Працюємо з потоком
Console.WriteLine(stream.Length);

// Тут файл ще відкритий!

// ... кінець методу
// stream.Dispose() викличеться тут автоматично

Ключові відмінності від класичного using:

  • Не потрібні фігурні дужки, не створюється вкладений блок коду.
  • Змінна доступна до кінця всього блоку, в якому вона оголошена (зазвичай — метод, іноді — цикл).
  • Звільнення ресурсу відбудеться лише тоді, коли блок виконання завершиться.

Чому це зручно?

  • Менше рівнів вкладеності — код стає значно коротшим і читабельнішим.
  • Легше працювати з кількома ресурсами — оголошуйте кілька using-змінних підряд, і все буде звільнено, коли завершиться метод.
  • Менше шансів помилитися — не пропустите область, де мали викликати Dispose().

4. Порівнюємо: класичний vs. сучасний using

Погляньмо на порівняння у вигляді коду.

Класичний спосіб


using (var reader = new StreamReader("input.txt"))
{
    using (var writer = new StreamWriter("output.txt"))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            writer.WriteLine(line.ToUpper());
        }
    }
} // Тут обидва файли будуть закриті

Сучасний спосіб (C# 8+)


using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");

string line;
while ((line = reader.ReadLine()) != null)
{
    writer.WriteLine(line.ToUpper());
}
// Обидва файли закриються тут, на виході з методу

Виглядає простіше, чи не так? Особливо якщо робити вкладеності ще більшими — сучасний спосіб значно полегшує роботу.

5. Коли і де спрацьовує Dispose()?

Тут часто помиляються: думають, що Dispose викличеться одразу після рядка використання, але це не так!

Подивіться на цей приклад:


void MyMethod()
{
    using var fileStream = new FileStream("data.bin", FileMode.Open);
    // ... багато коду, можливо, навіть циклів і вкладених викликів
    // fileStream ще відкритий!

    // Тут можемо отримати доступ до fileStream
}
// Тут, на } методу, викликається fileStream.Dispose()

Важливий момент: якщо оголосити using-змінну всередині циклу, Dispose буде викликано після кожної ітерації.


foreach (var path in filePaths)
{
    using var reader = new StreamReader(path);
    // працюємо з reader
} // reader.Dispose() викличеться після кожної ітерації (закриє файл)

6. Помилки при перенесенні старого коду

Іноді трапляється, що ви переносите старий код або копіюєте приклад із класичним using, а змінній потрібен більший «термін життя», ніж область видимості дужок. Тоді класичний варіант вам не підійде, а ось using-оголошення — ідеально.

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


using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
    using var resource2 = ...;
    // resource2 «живе» одну ітерацію
    // resource1 — усю функцію
}

7. Практика

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

Крок 1: Зберігаємо замовлення у файл


using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("Кава: Лате; Молоко: Вівсяне; Розмір: Великий");

// Другий параметр конструктора StreamWriter append: true вказує, що ми хочемо дописувати, а не перезаписувати файл.

Крок 2: Читаємо історію замовлень


using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine($"Замовлення: {line}");
}

Як тільки програма завершить виконання цього методу, файли закриються автоматично.

8. Найкращі практики роботи з using-оголошеннями

1. Завжди використовуйте using для обʼєктів, що реалізують IDisposable
У .NET більшість класів для роботи з файлами, потоками, ресурсами реалізують цей інтерфейс. Це сигнал: звільняйте мене через using!

2. Памʼятайте про видимість: не оголошуйте using-var там, де змінна потенційно «заважає»
Якщо змінна потрібна лише для кількох рядків — використовуйте її там, де треба, і не раніше.

3. Не забувайте про порядок звільнення
Якщо оголосити одразу кілька using-змінних підряд — Dispose буде викликано у зворотному порядку:


using var first = new Resource("First");
using var second = new Resource("Second");
// ... робота
// Спочатку Dispose для second, потім для first

Це іноді важливо, якщо один ресурс залежить від іншого (наприклад, потік запису має звільнитися раніше за файл).

4. Не використовуйте using-оголошення поза методом
using-оголошення заборонені на рівні класу (наприклад, для полів). Вони працюють лише всередині методів, конструкторів тощо.

5. Поєднуйте з обробкою помилок
Памʼятайте, що навіть із using не всі винятки зручні — доцільно додавати try-catch, якщо потрібен контроль над помилками читання/запису.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ