Menu

Оператор using в C#: IDisposable, Dispose и using var

Оператор using гарантирует вызов Dispose для файлов, потоков, подключений и других объектов IDisposable, даже когда выброшено исключение. Во что он компилируется, в каком порядке освобождаются объекты, объявление using из C# 8 и как реализовать IDisposable в собственном классе.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Некоторые объекты держат ресурсы, которыми сборщик мусора не управляет: открытые файлы, сетевые сокеты, подключения к базам данных, дескрипторы операционной системы. Они реализуют IDisposable, и вы освобождаете ресурс вызовом Dispose(). Оператор using вызывает его за вас в фиксированной точке, даже если выброшено исключение.

Эта страница об операторе using. Строки using System; в начале файла это другая возможность, директива using, которая импортирует пространство имён.

Блок using

using (declaration) { body } создаёт объект, выполняет тело, затем вызывает Dispose. Небольшой класс, который печатает из Dispose, показывает, когда именно это происходит:

Вывод:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

Второй блок выбрасывает исключение, и «close api» всё равно печатается до того, как catch обработает исключение. Эта гарантия и есть причина использовать using, а не вызывать Dispose() самостоятельно в последней строке: ручной вызов пропускается при любом исключении, выброшенном до него.

Во что компилируется using

Компилятор разворачивает блок using в try/finally. Эти две записи эквивалентны:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

Из этого разворачивания следуют три детали. Переменная видна только в блоке и доступна там только для чтения (переприсвоить writer нельзя). Объект должен реализовывать IDisposable, иначе код не компилируется. А значение null допустимо: Dispose просто пропускается, что удобно, когда фабрика может ничего не вернуть.

Файлы и потоки

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

Вывод:

id,total
1001,59.90
1002,12.50

Без Dispose последние строки средства записи могут остаться в его буфере, а в Windows открытый дескриптор не даёт другим программам (и вашему же следующему File.Delete) работать с файлом. Вспомогательные методы вроде File.WriteAllText и File.ReadAllLines открывают и освобождают файл внутри себя, поэтому using им не нужен. Подробнее на странице файлы.

Несколько ресурсов: порядок освобождения

Ставьте операторы using друг за другом без фигурных скобок между ними, чтобы открыть несколько ресурсов в одном блоке. Они освобождаются в порядке, обратном созданию:

Вывод:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

Обратный порядок это то, что нужно многослойным ресурсам: средство записи обёрнуто вокруг буфера, обёрнутого вокруг файла, поэтому средство записи должно сбросить данные в буфер до того, как буфер запишет их в файл, а это должно произойти до закрытия файла.

Объявление using (C# 8)

C# 8 добавил форму без фигурных скобок. Переменная освобождается, когда заканчивается объемлющая область видимости, обычно в конце метода:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Это убирает уровень отступа, что ощутимо, когда метод использует два-три ресурса. Правило порядка освобождения то же: объявления в одной области видимости освобождаются в обратном порядке. Цена в том, что ресурс остаётся открытым до конца области видимости, поэтому, когда файл нужно закрыть до того, как метод займётся другой работой, оставьте форму с блоком или поместите объявление в собственный блок { }.

Для объектов, освобождение которых асинхронное (IAsyncDisposable, например многие подключения к базам данных и потоки), в C# 8 есть также await using, который в том же месте ожидает DisposeAsync().

Реализация IDisposable в собственном классе

Реализуйте IDisposable, когда ваш класс владеет чем-то освобождаемым (он создал поток, таймер, подключение) и поэтому должен в свою очередь это освободить. В обычном случае это короткий метод:

Вывод:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

Здесь соблюдаются правила: Dispose освобождает всё, чем владеет объект; повторный вызов ничего не делает; использование объекта после освобождения выбрасывает ObjectDisposedException. using (log) также показывает, что блок может принимать существующую переменную вместо объявления.

Более длинный «шаблон dispose» с методом protected virtual void Dispose(bool disposing) и финализатором нужен только тогда, когда класс напрямую держит неуправляемый дескриптор (сырой указатель из нативного кода). В современном .NET оборачивайте такие дескрипторы в подкласс SafeHandle, и тогда вашему классу нужна лишь простая версия выше.

Частые ошибки

  • Возврат объекта, созданного в блоке using. Он освобождается при выходе из блока, поэтому вызывающий получает закрытый поток. Возвращайте данные или пусть using будет у вызывающего.
  • Забытый using у потоков и средств записи. Буферизованные данные могут потеряться, а файлы остаются заблокированными.
  • Освобождение общего объекта. Освобождайте только то, чем владеете. Например, долгоживущий HttpClient предназначен для совместного и повторного использования, а не для создания и освобождения на каждый запрос.
  • Использование объекта после блока. Методы освобождённого объекта выбрасывают ObjectDisposedException.
  • Расчёт на сборщик мусора. Он освобождает память, а не файлы или сокеты, и никогда не вызывает Dispose.

Часто задаваемые вопросы

Что делает оператор using в C#?

using (var x = ...) { ... } вызывает x.Dispose(), когда блок заканчивается, будь то нормально, через return или из-за выброшенного исключения. Компилятор превращает его в try/finally с вызовом Dispose в finally, поэтому ресурс освобождается в известной точке, а не тогда, когда отработает сборщик мусора.

Что такое IDisposable в C#?

IDisposable это интерфейс с одним методом, void Dispose(). Класс реализует его, когда держит что-то, что нужно явно освободить: дескриптор файла, сетевой сокет, подключение к базе данных, таймер. Любой объект, тип которого реализует IDisposable, нужно освобождать, когда работа с ним закончена, обычно через using.

Чем using отличается от using var?

using (var x = ...) { } освобождает объект в конце своего блока. У объявления using using var x = ...; (C# 8 и новее) собственного блока нет: объект освобождается, когда заканчивается объемлющая область видимости, обычно в конце метода. Это экономит уровень вложенности, когда ресурс должен жить до конца метода.

В каком порядке освобождаются несколько объектов using?

В порядке, обратном созданию. При using (var a = ...) using (var b = ...) { } сначала освобождается b, а затем a. Именно такой порядок нужен, когда b зависит от a, например StreamWriter, обёрнутый вокруг FileStream.

Вызывает ли сборщик мусора Dispose?

Нет. Сборщик мусора освобождает память и может когда-нибудь позже, в непредсказуемый момент, выполнить финализатор, но Dispose он никогда не вызывает. Неосвобождённый файл может остаться заблокированным, а его буферизованные данные могут так и не записаться. Для этого и существует using.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ