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. "Ручное" закрытие: не делайте так!

Это устаревший способ. И хоть я и показывал его в предыдущих примерах, в современной разработке его так почти никто не использует :P


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, если нужен контроль над ошибками чтения/записи.

2
Задача
C# SELF, 36 уровень, 0 лекция
Недоступна
Чтение файла с помощью `using var`
Чтение файла с помощью `using var`
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Александр Уровень 39
15 февраля 2026
плохо что всё на примере файлов. хотелось бы на примере сетевых стримов, так как это более актуально в наше время