Как предотвратить распространение IDisposable на все ваши классы?

StackOverflow https://stackoverflow.com/questions/661815

Вопрос

Начните с этих простых занятий...

Допустим, у меня есть простой набор таких классов:

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 должно быть IDisposableBus создает 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))}));
        }
    }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top