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
Dispose objects before losing scope (CA2000)
Существует несколько стандартных Roslyn анализаторов, которые находят проблемы с использованием IDisposable:
- CA2000
- CA2213
Документация к анализатору CA2000 даёт такое описание:
If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead.
Этот анализатор находит места, где внутри метода создался экземпляр класса с IDisposable реализацией, но не был вызван метод Dispose. Основная проблема данного анализатора в том, что он не умеет находить owner transferring и генерирует из-за этого false positive. Подробно это описано в ишуях https://github.com/dotnet/roslyn-analyzers/issues/1617 и https://github.com/dotnet/runtime/issues/29631.
Но такое правило бы генерировало слишком много false positive. Поэтому для поддержки фабричных методов была сделано ещё одно допущение: если экземпляр создаётся и отдаётся возвращаемым значением, то это также передаёт ownership вызывающему коду:
Со стороны вызывающего кода также работает такая семантика:
CA2000: Избавление от false positive
При этом есть несколько неочевидных и не задокументированных способов повлиять на поведение анализатора, чтобы снизить количество false positive.
Первый способ - это “Special cases” для Stream, StreamReader и подобных типов. Особенность заключается в том, что для этих типов в коде анализатора прописано, что они берут на себя ownership, и значит любой инстанс, который передаётся в конструктор StreamReader будет освобождаться от необходимость быть задиспоуженым явно.
Второй способ - это конфигурации анализатора с помощь dispose_ownership_transfer_at_constructor
и dispose_ownership_transfer_at_method_call
.
При выставлении at_constructor
Roslyn Data flow analys будет считать, что для всех передаваемых IDisposable экземпляров также передаётся и ownership .
Но данные опции ослабляют анализатор и открывают возможность для false negative. Без опции вызов Dispose требуется от Method и не требуется от конструктора. С включённой опцией вызов Dispose не требуется ни от Method, ни от конструктора.
Описание этих опций можно найти на GitHub.