Перейти к содержимому
Теория · Исключения · урок 7 из 9

using и IDisposable

Правило «уходя, выключите плиту» можно не держать в голове, а встроить в саму плиту. У некоторых плит есть автоматическое отключение: вы уходите из кухни, и плита выключается сама, как бы вы ни ушли. В C# такую автоматику даёт инструкция using.

Зачем она нужна? Некоторые объекты держат то, что надо освободить, когда работа закончена: открытый файл, сетевое соединение. Если этого не сделать, файл останется занят, а соединение — открыто.

Интерфейс IDisposable

Если объект держит файл или соединение, его класс реализует интерфейс IDisposable. В нём один метод — Dispose(): он освобождает то, что держит объект.

Вызвать Dispose() можно и вручную. Но если между созданием объекта и этим вызовом случится исключение, до вызова дело не дойдёт. Вызов в блоке finally выполнится и при исключении, а using — короткая форма такого блока.

Инструкцию using не путайте с директивой using из темы о строках: директива подключает к файлу пространство имён, а инструкция вызывает Dispose() в конце блока.

Две формы using

Инструкция using

Объект создают в скобках после using, а работу с ним пишут в фигурных скобках:

C#
using (var stove = new Stove())   // Stove on
{
    Console.WriteLine("Cooking"); // Cooking
}                                 // Stove off
Console.WriteLine("Done");        // Done

class Stove : IDisposable
{
    public Stove() => Console.WriteLine("Stove on");
    public void Dispose() => Console.WriteLine("Stove off");
}

Давайте разберём программу по шагам:

  1. new Stove() создаёт плиту, и конструктор печатает Stove on.
  2. using (var stove = …) запоминает объект, чтобы в конце блока вызвать у него Dispose().
  3. Тело блока печатает Cooking.
  4. Когда выполнение доходит до закрывающей скобки, using вызывает stove.Dispose(), и печатается Stove off.
  5. Затем выполняется строка после блока и печатает Done.

Объявление using var

Короткая форма обходится без скобок и своего блока: using var stove = new Stove();. Dispose() вызывается в конце того блока, где стоит объявление. Это происходит, даже когда исключение покидает блок:

C#
try
{
    using var stove = new Stove(); // Stove on
    Console.WriteLine(int.Parse("x"));
}                                  // Stove off
catch (FormatException)
{
    Console.WriteLine("Not a number"); // Not a number
}

Этот фрагмент продолжает первую программу: поставьте его перед классом Stove. Разберём и этот пример:

  1. Объявление создаёт плиту, и печатается Stove on.
  2. int.Parse("x") выбрасывает исключение, и оно покидает блок try.
  3. Когда исключение покидает блок, using вызывает Dispose(), и печатается Stove off.
  4. Только после этого catch печатает Not a number.

Аналогия с автоматикой неточна в одном. Dispose() вызывается не тогда, когда объект перестал быть нужен, а строго в конце блока. Пока блок не кончился, плита включена, даже если вы ею уже не пользуетесь.

using — это try и finally

Этот вопрос часто задают на собеседовании: во что компилятор превращает using? Почти в такой код:

C#
var stove = new Stove();          // Stove on
try
{
    Console.WriteLine("Cooking"); // Cooking
}
finally
{
    stove.Dispose();              // Stove off
}

class Stove : IDisposable
{
    public Stove() => Console.WriteLine("Stove on");
    public void Dispose() => Console.WriteLine("Stove off");
}

Объект создаётся до try, тело блока попадает в try, а вызов Dispose() — в finally. Поэтому using вызывает Dispose() на любом выходе из блока: в конце, после return и после исключения.

Частая ошибка

В using положили объект класса, который не реализует IDisposable:

C#
using (var box = new Box()) // ✗
{
    Console.WriteLine("Inside");
}

class Box
{
    public Box() => Console.WriteLine("Box ready");
}
TEXT
error CS1674: 'Box': type used in a using statement must implement 'System.IDisposable'.

Программа не компилируется. using принимает только типы, которые реализуют IDisposable, а Box его не реализует. Свой метод Dispose() тоже не помог бы: компилятор ищет именно интерфейс. using нужен только объектам, которым есть что освобождать, а обычный объект создают без него: var box = new Box();.

Главное

  • Класс, объект которого держит файл или соединение, реализует IDisposable и его метод Dispose().
  • using (var x = …) { } и using var x = …; вызывают Dispose() в конце блока, даже если блок покинуло исключение.
  • Компилятор превращает using в try и finally с вызовом Dispose() в finally.
  • using с объектом класса без IDisposable не компилируется: ошибка CS1674.