Как предотвратить распространение IDisposable на все ваши классы?
-
20-08-2019 - |
Вопрос
Начните с этих простых занятий...
Допустим, у меня есть простой набор таких классов:
class Bus
{
Driver busDriver = new Driver();
}
class Driver
{
Shoe[] shoes = { new Shoe(), new Shoe() };
}
class Shoe
{
Shoelace lace = new Shoelace();
}
class Shoelace
{
bool tied = false;
}
А Bus
имеет Driver
, Driver
имеет два Shoe
с, каждый Shoe
имеет Shoelace
.Все очень глупо.
Добавьте объект IDisposable в Shoelace.
Позже я решаю, что какая-то операция над Shoelace
может быть многопоточным, поэтому я добавляю EventWaitHandle
для потоков для связи.Так Shoelace
теперь выглядит так:
class Shoelace
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
// ... other stuff ..
}
Реализация IDisposable на шнурках
Но сейчас Microsoft FxCop будет жаловаться: «Реализуйте IDisposable в «Шнурке», поскольку он создает члены следующих типов IDisposable:«EventWaitHandle».
Хорошо, я реализую IDisposable
на Shoelace
и мой аккуратный маленький класс превращается в ужасный беспорядок:
class Shoelace : IDisposable
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~Shoelace()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
if (waitHandle != null)
{
waitHandle.Close();
waitHandle = null;
}
}
// No unmanaged resources to release otherwise they'd go here.
}
disposed = true;
}
}
Или (как отмечают комментаторы), поскольку Shoelace
сам по себе не имеет неуправляемых ресурсов, я мог бы использовать более простую реализацию удаления без необходимости Dispose(bool)
и деструктор:
class Shoelace : IDisposable
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
public void Dispose()
{
if (waitHandle != null)
{
waitHandle.Close();
waitHandle = null;
}
GC.SuppressFinalize(this);
}
}
С ужасом наблюдайте за распространением IDisposable
Верно, это исправлено.Но теперь FxCop будет жаловаться, что Shoe
создает Shoelace
, так Shoe
должно быть IDisposable
слишком.
И Driver
создает Shoe
так Driver
должно быть IDisposable
.И Bus
создает Driver
так Bus
должно быть IDisposable
и так далее.
Внезапно моя маленькая перемена Shoelace
у меня много работы, и мой начальник задается вопросом, зачем мне нужно оформлять заказ Bus
внести изменения в Shoelace
.
Вопрос
Как предотвратить такое распространение IDisposable
, но при этом убедиться, что ваши неуправляемые объекты расположены правильно?
Решение
Вы не можете «предотвратить» распространение IDisposable.Некоторые классы необходимо удалить, например AutoResetEvent
, и наиболее эффективный способ — сделать это в Dispose()
метод, чтобы избежать накладных расходов на финализаторы.Но этот метод должен быть каким-то образом вызван, поэтому, как и в вашем примере, классы, которые инкапсулируют или содержат IDisposable, должны их утилизировать, поэтому они также должны быть одноразовыми и т. д.Единственный способ избежать этого – это:
- по возможности избегайте использования классов IDisposable, блокируйте или ожидайте событий в одном месте, храните дорогостоящие ресурсы в одном месте и т. д.
- создавайте их только тогда, когда они вам нужны, и удаляйте их сразу после этого (
using
шаблон)
В некоторых случаях IDisposable можно игнорировать, поскольку он поддерживает необязательный регистр.Например, WaitHandle реализует IDisposable для поддержки именованного мьютекса.Если имя не используется, метод Dispose ничего не делает.MemoryStream — еще один пример: он не использует системные ресурсы, и его реализация Dispose также ничего не делает.Тщательное размышление о том, используется ли неуправляемый ресурс или нет, может быть полезным.То же самое можно сделать, исследуя доступные исходные коды библиотек .net или используя декомпилятор.
Другие советы
С точки зрения корректности, вы не можете предотвратить распространение IDisposable через объектные отношения, если родительский объект создает и по существу владеет дочерним объектом, который теперь должен быть одноразовым.В этой ситуации корректен FxCop, и родительский элемент должен быть IDisposable.
Что вы можете сделать, так это избежать добавления IDisposable в конечный класс в иерархии объектов.Это не всегда легкая задача, но интересное упражнение.С логической точки зрения нет причин, по которым шнурки ShoeLace должны быть одноразовыми.Вместо добавления сюда WaitHandle можно также добавить связь между ShoeLace и WaitHandle в момент его использования.Самый простой способ — через экземпляр Dictionary.
Если вы можете переместить WaitHandle в свободную ассоциацию с помощью карты в точке фактического использования WaitHandle, вы можете разорвать эту цепочку.
Предотвращать IDisposable
Чтобы не распространяться, вам следует попытаться инкапсулировать использование одноразового объекта внутри одного метода.Попробуйте спроектировать Shoelace
иначе:
class Shoelace {
bool tied = false;
public void Tie() {
using (var waitHandle = new AutoResetEvent(false)) {
// you can even pass the disposable to other methods
OtherMethod(waitHandle);
// or hold it in a field (but FxCop will complain that your class is not disposable),
// as long as you take control of its lifecycle
_waitHandle = waitHandle;
OtherMethodThatUsesTheWaitHandleFromTheField();
}
}
}
Область действия дескриптора ожидания ограничена Tie
метод, и классу не обязательно иметь одноразовое поле, и поэтому он сам не должен быть одноразовым.
Поскольку дескриптор ожидания является деталью реализации внутри Shoelace
, он не должен каким-либо образом изменять свой общедоступный интерфейс, например добавлять новый интерфейс в свое объявление.Что будет потом, когда одноразовое поле вам больше не понадобится, вы уберете IDisposable
декларация?Если вы думаете о Shoelace
абстракция, вы быстро понимаете, что он не должен быть загрязнен инфраструктурными зависимостями, например IDisposable
. IDisposable
должен быть зарезервирован для классов, абстракция которых инкапсулирует ресурс, требующий детерминированной очистки;то есть для классов, где одноразовость является частью абстракция.
Это во многом похоже на проблему дизайна более высокого уровня, как это часто бывает, когда «быстрое решение» превращается в трясину.Для более подробного обсуждения путей выхода из ситуации вы можете найти эта тема полезный.
Интересно, если Driver
определяется, как указано выше:
class Driver
{
Shoe[] shoes = { new Shoe(), new Shoe() };
}
Потом, когда Shoe
сделан IDisposable
, FxCop (v1.36) на это не жалуется Driver
также должно быть IDisposable
.
Однако, если это определено следующим образом:
class Driver
{
Shoe leftShoe = new Shoe();
Shoe rightShoe = new Shoe();
}
тогда он будет жаловаться.
Подозреваю, что это всего лишь ограничение FxCop, а не решение, потому что в первой версии Shoe
экземпляры все еще создаются Driver
и еще надо как-то утилизировать.
Я не думаю, что существует технический способ предотвратить распространение IDisposable, если вы держите свой дизайн настолько тесно связанным.Тогда следует задаться вопросом, правильный ли дизайн.
В вашем примере, я думаю, имеет смысл, чтобы шнурок принадлежал обуви, и, возможно, водитель должен владеть своей обувью.Однако автобус не должен принадлежать водителю.Обычно водители автобусов не следуют за автобусами на свалку :) Что касается водителей и обуви, водители редко делают обувь самостоятельно, то есть на самом деле они ею не «владеют».
Альтернативная конструкция может быть:
class Bus
{
IDriver busDriver = null;
public void SetDriver(IDriver d) { busDriver = d; }
}
class Driver : IDriver
{
IShoePair shoes = null;
public void PutShoesOn(IShoePair p) { shoes = p; }
}
class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
Shoelace lace = new Shoelace();
}
class Shoelace : IDisposable
{
...
}
К сожалению, новый дизайн более сложен, поскольку требует дополнительных классов для создания и удаления конкретных экземпляров обуви и драйверов, но эта сложность присуща решаемой проблеме.Хорошо, что автобусы больше не должны быть одноразовыми только для того, чтобы выбросить шнурки.
По сути, это то, что происходит, когда вы смешиваете композицию или агрегацию с одноразовыми классами.Как уже упоминалось, первым выходом было бы реорганизовать waitHandle из шнурка.
Сказав это, вы можете значительно сократить шаблон Disposable, если у вас нет неуправляемых ресурсов.(Я все еще ищу официальную ссылку на это.)
Но вы можете опустить деструктор и GC.SuppressFinalize(this);и, возможно, немного очистить виртуальную пустоту Dispose (bool dispositing).
Как насчет использования инверсии управления?
class Bus
{
private Driver busDriver;
public Bus(Driver busDriver)
{
this.busDriver = busDriver;
}
}
class Driver
{
private Shoe[] shoes;
public Driver(Shoe[] shoes)
{
this.shoes = shoes;
}
}
class Shoe
{
private Shoelace lace;
public Shoe(Shoelace lace)
{
this.lace = lace;
}
}
class Shoelace
{
bool tied;
private AutoResetEvent waitHandle;
public Shoelace(bool tied, AutoResetEvent waitHandle)
{
this.tied = tied;
this.waitHandle = waitHandle;
}
}
class Program
{
static void Main(string[] args)
{
using (var leftShoeWaitHandle = new AutoResetEvent(false))
using (var rightShoeWaitHandle = new AutoResetEvent(false))
{
var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
}
}
}