IDisposing - это интерфейс с единственным методом Dispose. Концептуально этот метод нужен для того, чтобы выполнить код после того, как объект перестанет использоваться. Пример ситуации, когда это необходимо - это работа с внешними ресурсами. При создании некоторых объектов для работы с файлами, приложение захватывает файловый дескриптор у операционной системы и не даёт другим приложениям работать с файлом, пока приложение не даст сигнал, что дескриптор больше не удерживается.
var file = new File();
// Some operation with file
file.Dispose();
Для упрощения работы с IDisposable в C# есть конструкция using
:
using (var file = new File())
{
// Some operation with file
} // .Dispose will call after exit from statement context
Ownership transferring
Простые сценарии работы с IDisposable - это создание, использование и вызов Dispose в рамках одного метода. Но экземпляр может передаваться в другие методы. Microsoft рекомендуют придерживаться алгоритм: “если создаётся диспосабельный экземпляр, то нужно явно вызывать метод Dispose”:
public void F1()
{
using (var file = new File())
Do(file);
}
Но есть сценарий, когда такой подход не сработает - создание экземпляра. Допустим, что нам нужно создать экземпляр класса MyClass. Одним из аргументов конструктора данного класса является тип, который реализует IDisposable. И допустим, что код создание экзмпляра нужно вынести в фабричный метод. Получим такой код:
public MyClass Create()
{
var file = new File();
return new MyClass(file);
}
В данном случае происходит ownership transferring, экземпляр File передаётся в MyClass и ожидается, что он будет управлять жизненным циклом и вызовет метод Dispose в своём методе Dispose. Ещё одним примером является API типа SteamReader:
using (var reader = new StreamReader(new FileStream("path", FileMode.Create)))
По умолчанию считается, что StreamReader в такой ситуации захватывает управление экземпляром FileStream.
Повторные вызовы Dispose
В рекомендациях от Microsoft указано, что вызов Dispose метода должен быть идемпотентным. Повторные вызовы не должны бросать ошибки или приводить к другим сайд-эффектам. Но к сожалению не все разработчики библиотек следуют этому и даже стандартные типы не всегда себя так ведут. Так например, повторные вызовы Dispose у TcpClient генерируют ошибки о том, что объект уже был задиспожен.
Более подробно описано тут: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose.
Вызовы Dispose в конструкторе
Dispose – это метод экземпляра и вызывается на нём. Из этого следует очень неприятный вывод: “нельзя вызвать Dispose, если нет объекта”. Представим ситуацию, что в конструкторе создаётся два экземпляра класса Stream и сохраняются в поля:
public class MyType : IDisposable
{
private readonly Stream _stream1;
private readonly Stream _stream2;
public MyType()
{
_stream1 = new FileStream();
_stream2 = new FileStream();
}
public void Dispose()
{
_stream1.Dispose();
_stream2.Dispose();
}
}
using (var value = new MyType())
// Some logic
Такая реализация не будет корректно обрабатывать ситуацию, когда создание второго потока завершится с ошибкой. Это приведёт к тому, что экземпляр типа MyType не будет создан, а значит у него не может быть вызван Dispose. Созданный экземпляр _stream1 останется без owner’а. Варианты решения проблемы:
- Обернуть код создания экземпляров FileStream в try/catch и освобождать первый, если второй не создался
- Обернут код в конструкторе в try/catch и вызывать Dispose