$ sudo teach IT
Модуль 8 · Функции

Урок 8.8 — Контекстный менеджер using

Автоматическое управление ресурсами: файлы, соединения, блокировки.

Когда твоя программа открывает файл, подключается к базе данных или захватывает блокировку — она использует системные ресурсы. Если их вовремя не освободить, программа начнёт «течь» ресурсами. На сервере это приведёт к тому, что через пару часов перестанут открываться файлы, а база данных откажется принимать подключения. Конструкция using гарантирует, что ресурсы будут освобождены, даже если произошла ошибка.

⚠️

Проблема управления ресурсами

В C# есть сборщик мусора (GC), который освобождает память от неиспользуемых объектов. Но GC не управляет неуправляемыми ресурсами: файловые дескрипторы, подключения к БД, сокеты, графические объекты GDI+.

Что произойдёт, если не закрыть файл?

C# · Проблемный код
// ❌ ПЛОХО: файл может не закрыться
static void ReadFileBad()
{
    var reader = new StreamReader("file.txt");
    string content = reader.ReadToEnd();
    Console.WriteLine(content);
    // Если до reader.Close() произойдёт исключение —
    // файл останется открытым!
    reader.Close();
}

// ❌ ПЛОХО: ручное управление — легко забыть
static void ReadFileMessy()
{
    var reader = new StreamReader("file.txt");
    try
    {
        string content = reader.ReadToEnd();
        Console.WriteLine(content);
    }
    finally
    {
        reader.Close();  // гарантированно закроем
    }
}

Ручное управление через try-finally работает, но оно громоздкое и подвержено ошибкам. Особенно когда нужно открыть несколько ресурсов.

📦

Конструкция using (var resource = new Resource()) { }

Конструкция using — это синтаксический сахар для паттерна try-finally. Она гарантирует, что метод Dispose() будет вызван по завершении блока, даже при исключении.

Синтаксис:

using (var resource = new Resource()) { /* использование */ }

C# · Using — правильный способ
// ✅ ХОРОШО: using гарантирует закрытие
static void ReadFileGood()
{
    using (var reader = new StreamReader("file.txt"))
    {
        string content = reader.ReadToEnd();
        Console.WriteLine(content);
    }
    // reader.Dispose() вызван автоматически
}

// Можно вкладывать 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());
    }
}
💡 Не путай using-директиву (для импорта пространств имён) с using-конструкцией (для управления ресурсами). Это разные вещи с одинаковым ключевым словом!
🔌

Интерфейс IDisposable и метод Dispose()

IDisposable — это интерфейс, содержащий единственный метод Dispose(). Любой класс, который использует неуправляемые ресурсы, должен реализовать IDisposable. Конструкция using работает только с объектами, реализующими IDisposable.

C# · Реализация IDisposable
public class FileManager : IDisposable
{
    private FileStream? _stream;
    private bool _disposed = false;

    public FileManager(string filePath)
    {
        _stream = File.Open(filePath, FileMode.OpenOrCreate);
        Console.WriteLine($"Файл {filePath} открыт");
    }

    public void WriteData(string data)
    {
        if (_disposed)
            throw new ObjectDisposedException(nameof(FileManager));

        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
        _stream?.Write(bytes, 0, bytes.Length);
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            _stream?.Close();
            _stream?.Dispose();
            _stream = null;
            _disposed = true;
            Console.WriteLine("Ресурсы освобождены");
        }
    }
}

// Использование:
using (var fm = new FileManager("test.txt"))
{
    fm.WriteData("Hello, World!");
}  // Dispose() вызван автоматически

Паттерн Dispose (правильная реализация):

  • Флаг _disposed — предотвращает повторное освобождение.
  • Проверка _disposed в методах — защита от использования после освобождения.
  • Для наследников используют protected virtual void Dispose(bool disposing).
  • Вызов GC.SuppressFinalize(this) — финализация не нужна, если Dispose вызван.
🔄

finally как альтернатива using

До появления using (C# 1.0) разработчики использовали try-finally. Эта конструкция тоже гарантирует выполнение кода в блоке finally, независимо от того, было ли исключение.

C# · Finally как альтернатива
// Эквивалент using через try-finally:
StreamReader reader = null;
try
{
    reader = new StreamReader("file.txt");
    Console.WriteLine(reader.ReadToEnd());
}
finally
{
    if (reader != null)
        reader.Dispose();
}

// То же самое с using:
using (var r = new StreamReader("file.txt"))
{
    Console.WriteLine(r.ReadToEnd());
}

// using короче, читабельнее и не требует ручной проверки на null!
💡 using — это и есть try-finally «под капотом». Компилятор транслирует using в try-finally с вызовом Dispose().
✨

Using declaration (C# 8+): using var resource = new Resource()

Начиная с C# 8.0, появилась более лаконичная форма — using declaration. Теперь не нужно писать фигурные скобки: ресурс будет освобождён в конце текущей области видимости.

C# · Using declaration
// C# 8+ — using declaration:
static void ReadFileModern()
{
    using var reader = new StreamReader("file.txt");
    string content = reader.ReadToEnd();
    Console.WriteLine(content);
    // Dispose() будет вызван при выходе из метода
}

// Сравните с классическим using:
static void ReadFileClassic()
{
    using (var reader = new StreamReader("file.txt"))
    {
        string content = reader.ReadToEnd();
        Console.WriteLine(content);
    }
}

// Using declaration удобен для нескольких ресурсов:
static void ProcessFiles()
{
    using var input = new StreamReader("input.txt");
    using var output = new StreamWriter("output.txt");

    string? line;
    while ((line = input.ReadLine()) != null)
    {
        output.WriteLine(line.ToUpper());
    }
    // input и output будут освобождены в обратном порядке
}

Когда использовать какой стиль?

  • using declaration (using var) — для временных ресурсов внутри метода, когда не нужен явный блок.
  • Классический using (using { }) — когда нужно чётко ограничить время жизни ресурса: «открыл — использовал — закрыл до конца блока».
  • try-finally — только если using недоступен (например, старый код, .NET Framework 4.x).
📚

Встроенные классы, реализующие IDisposable

Многие классы .NET реализуют IDisposable. Вот наиболее часто используемые:

StreamReader / StreamWriter (работа с файлами)

C# · StreamReader + using
using var reader = new StreamReader("data.txt");
string firstLine = reader.ReadLine() ?? "";

using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello");

SqlConnection (подключение к БД)

C# · SqlConnection
using var connection = new SqlConnection("connection string");
connection.Open();
// работа с БД
// соединение автоматически закроется при выходе из блока

HttpClient (HTTP-запросы)

C# · HttpClient
// ⚠️ Внимание: HttpClient обычно используют как static/singleton
// Но в краткосрочных сценариях:
using var client = new HttpClient();
var response = await client.GetStringAsync("https://example.com");
Console.WriteLine(response);

Другие IDisposable классы

  • FileStream, MemoryStream, BufferedStream
  • SqlConnection, SqlCommand, SqlDataReader
  • StreamReader, StreamWriter, BinaryReader, BinaryWriter
  • ZipArchive, ZipFile
  • Bitmap, Font, Pen, Brush (GDI+)
  • Mutex, Semaphore, Process
  • NetworkStream, SslStream, TcpClient
❓

Мини-тест

Вопрос 1: Для чего нужна конструкция using?

A) Для импорта пространств имён
B) Для гарантированного освобождения ресурсов
C) Для объявления переменных
D) Для условного выполнения

Правильный ответ: B

Вопрос 2: Какой интерфейс должен реализовать класс, чтобы его можно было использовать в using?

A) IComparable
B) IEnumerable
C) IDisposable
D) ICloneable

Правильный ответ: C

Вопрос 3: Когда вызывается Dispose() при использовании using declaration?

A) Немедленно после создания переменной
B) При выходе из текущей области видимости
C) После каждой операции с ресурсом
D) Через определённое время

Правильный ответ: B

Вопрос 4: Что выведет код?

using var r = new StreamReader("test.txt");
// ... чтение
// Dispose() будет вызван?
A) Да, при выходе из метода
B) Нет, нужно явно вызвать
C) Только если нет исключения
D) Только при сборке мусора

Правильный ответ: A

Вопрос 5: В какой языковой версии появился using declaration (using var)?

A) C# 6.0
B) C# 7.0
C) C# 8.0
D) C# 9.0

Правильный ответ: C

📌

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

1️⃣
Неуправляемые ресурсы (файлы, соединения, сокеты) нужно освобождать вручную. Сборщик мусора с ними не работает.
2️⃣
Конструкция using гарантирует вызов Dispose(), даже при исключении. Альтернатива — try-finally.
3️⃣
IDisposable — интерфейс с методом Dispose(). Реализуй его в своих классах, если они используют неуправляемые ресурсы.
4️⃣
Using declaration (using var) — современный стиль C# 8+. Ресурс освобождается при выходе из текущей области видимости.
5️⃣
StreamReader, StreamWriter, SqlConnection, HttpClient — примеры встроенных классов, реализующих IDisposable. Всегда оборачивай их в using.

Тест: 8.8: Контекстный менеджер using

3 вопроса

Using

Premium