Как вы предотвращаете распространение IDisposable на все ваши классы?
Начните с этих простых классов...
Скажем, у меня есть простой набор таких классов:
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;
}
A Bus
имеет a Driver
, Driver
имеет два Shoe
s, каждый Shoe
имеет Shoelace
. Все очень глупо.
Добавить объект IDisposable к Shoelace
Позже я решил, что некоторые операции с Shoelace
могут быть многопоточными, поэтому я добавляю EventWaitHandle
для потоков, с которыми можно связаться. Итак, Shoelace
теперь выглядит следующим образом:
class Shoelace
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
// ... other stuff ..
}
Внедрить IDisposable на шнурках
Но теперь Microsoft FxCop будет жаловаться: "Внедрить IDisposable в" Shoelace ", потому что он создает члены следующих типов 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 без необходимости Dispose(bool)
и Destructor:
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 spreads
Правильно, что это исправлено. Но теперь FxCop будет жаловаться, что Shoe
создает Shoelace
, поэтому Shoe
тоже должен быть IDisposable
.
И Driver
создает Shoe
, поэтому Driver
должен быть IDisposable
.
И Bus
создает Driver
, поэтому Bus
должен быть IDisposable
и т.д.
Неожиданно мое небольшое изменение на Shoelace
вызывает у меня много работы, и мой босс задается вопросом, почему мне нужно проверить Bus
, чтобы внести изменения в Shoelace
.
Вопрос
Как вы предотвращаете это распространение IDisposable
, но все же убедитесь, что неуправляемые объекты правильно расположены?
Ответы
Ответ 1
Вы не можете "предотвратить" распространение IDisposable. Некоторые классы должны быть удалены, например AutoResetEvent
, а самый эффективный способ - сделать это в методе Dispose()
, чтобы избежать накладных расходов финализаторов. Но этот метод нужно называть каким-то образом, так как в вашем примере классы, которые инкапсулируют или содержат IDisposable, должны их уничтожать, поэтому они также должны быть одноразовыми и т.д. Единственный способ избежать этого:
- избегать использования доступных классов IDisposable, блокировать или ждать событий в отдельных местах, хранить дорогостоящие ресурсы в одном месте и т.д.
- создайте их только тогда, когда они вам понадобятся, и удалите их сразу после (шаблон
using
)
В некоторых случаях IDisposable можно игнорировать, поскольку он поддерживает необязательный случай. Например, WaitHandle реализует IDisposable для поддержки именованного Mutex. Если имя не используется, метод Dispose ничего не делает. Еще один пример - MemoryStream, он не использует системные ресурсы, а реализация Dispose также ничего не делает. Тщательное размышление о том, используется ли неуправляемый ресурс или нет, может быть учебным. Таким образом, можно изучить доступные источники для библиотек .net или использовать декомпилятор.
Ответ 2
С точки зрения правильности, вы не можете предотвратить распространение IDisposable через отношение объекта, если родительский объект создает и по существу владеет дочерним объектом, который теперь должен быть одноразовым. FxCop корректен в этой ситуации, и родительский объект должен быть IDisposable.
Что вы можете сделать, так это избегать добавления IDisposable в класс листа в иерархии объектов. Это не всегда легкая задача, но это интересное упражнение. С логической точки зрения нет причин, по которым ShoeLace должен быть одноразовым. Вместо добавления WaitHandle здесь также можно добавить связь между ShoeLace и WaitHandle в той точке, в которой она была использована. Самый простой способ - через экземпляр словаря.
Если вы можете переместить WaitHandle в свободную ассоциацию с помощью карты в точке, где WaitHandle фактически используется, вы можете разбить эту цепочку.
Ответ 3
Чтобы предотвратить распространение 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();
}
}
}
Объем дескриптора wait ограничен методом Tie
, и классу не нужно иметь одноразовое поле, и поэтому его не нужно будет располагать самостоятельно.
Поскольку дескриптор wait является деталью реализации внутри Shoelace
, он не должен каким-либо образом изменять его публичный интерфейс, например добавить новый интерфейс в его объявление. Что произойдет тогда, когда вам больше не понадобится одноразовое поле, вы удалите объявление IDisposable
? Если вы думаете об Shoelace
абстракции, вы быстро поймете, что это не должно быть загрязнено зависимостями инфраструктуры, например IDisposable
. IDisposable
следует зарезервировать для классов, абстракция которых инкапсулирует ресурс, который требует детерминированной очистки; то есть для классов, где одноразовость является частью абстракции .
Ответ 4
Это очень похоже на проблему с дизайном более высокого уровня, как это часто бывает, когда "быстрое исправление" переходит в трясину. Для более подробного обсуждения путей выхода вы можете найти этот поток полезным.
Ответ 5
Интересно, если 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
и все равно должны быть расположены как-то.
Ответ 6
Я не думаю, что существует технический способ предотвращения распространения 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
{
...
}
Новый дизайн, к сожалению, более сложный, так как он требует дополнительных классов для создания и использования конкретных экземпляров обуви и драйверов, но это осложнение присуще решаемой проблеме. Хорошо то, что автобусы больше не должны быть одноразовыми просто для того, чтобы утилизировать шнурки.
Ответ 7
Это в основном то, что происходит, когда вы смешиваете состав или агрегацию с одноразовыми классами. Как уже упоминалось, первым выходом было бы реорганизовать waitHandle из шнурки.
Сказав это, вы можете значительно разделить шаблон "Одноразовый", если у вас нет неуправляемых ресурсов. (Я все еще ищу официальную ссылку для этого.)
Но вы можете опустить деструктор и GC.SuppressFinalize(this); и, возможно, очистите виртуальную пустоту Dispose (bool disposing) немного.
Ответ 8
Как использовать инверсию управления?
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))}));
}
}
}