IDisposing - это интерфейс с единственным методом Dispose. Концептуально этот метод нужен для того, чтобы выполнить код после того, как объект перестанет использоваться. Пример ситуации, когда это необходимо - это работа с внешними ресурсами. При создании некоторых объектов для работы с файлами, приложение захватывает файловый дескриптор у операционной системы и не даёт другим приложениям работать с файлом, пока приложение не даст сигнал, что дескриптор больше не удерживается.
Для упрощения работы с IDisposable в C# есть конструкция using
:
Ownership transferring
Простые сценарии работы с IDisposable - это создание, использование и вызов Dispose в рамках одного метода. Но экземпляр может передаваться в другие методы. Microsoft рекомендуют придерживаться алгоритм: “если создаётся диспосабельный экземпляр, то нужно явно вызывать метод Dispose”:
Но есть сценарий, когда такой подход не сработает - создание экземпляра. Допустим, что нам нужно создать экземпляр класса MyClass. Одним из аргументов конструктора данного класса является тип, который реализует IDisposable. И допустим, что код создание экзмпляра нужно вынести в фабричный метод. Получим такой код:
В данном случае происходит ownership transferring, экземпляр File передаётся в MyClass и ожидается, что он будет управлять жизненным циклом и вызовет метод Dispose в своём методе Dispose. Ещё одним примером является API типа SteamReader:
По умолчанию считается, что StreamReader в такой ситуации захватывает управление экземпляром FileStream.
Повторные вызовы Dispose
В рекомендациях от Microsoft указано, что вызов Dispose метода должен быть идемпотентным. Повторные вызовы не должны бросать ошибки или приводить к другим сайд-эффектам. Но к сожалению не все разработчики библиотек следуют этому и даже стандартные типы не всегда себя так ведут. Так например, повторные вызовы Dispose у TcpClient генерируют ошибки о том, что объект уже был задиспожен.
Более подробно описано тут: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose.
Вызовы Dispose в конструкторе
Dispose – это метод экземпляра и вызывается на нём. Из этого следует очень неприятный вывод: “нельзя вызвать Dispose, если нет объекта”. Представим ситуацию, что в конструкторе создаётся два экземпляра класса Stream и сохраняются в поля:
Такая реализация не будет корректно обрабатывать ситуацию, когда создание второго потока завершится с ошибкой. Это приведёт к тому, что экземпляр типа MyType не будет создан, а значит у него не может быть вызван Dispose. Созданный экземпляр _stream1 останется без owner’а. Варианты решения проблемы:
- Обернуть код создания экземпляров FileStream в try/catch и освобождать первый, если второй не создался
- Обернут код в конструкторе в try/catch и вызывать Dispose