Некоторые объекты держат ресурсы, которыми сборщик мусора не управляет: открытые файлы, сетевые сокеты, подключения к базам данных, дескрипторы операционной системы. Они реализуют 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.