Как вы предотвращаете распространение 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))}));
        }
    }
}