Как предотвратить распространение 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 есть Driver на Driver два Shoe s, каждый Shoe есть Shoelace. Все очень глупо.

добавить IDisposable объект для шнурка

позже я решаю, что некоторые операции на Shoelace может быть многопоточным, поэтому я добавить EventWaitHandle для потоков для связи с. Так что Shoelace теперь выглядит это:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

реализовать IDisposable на шнурке

а теперь Microsoft FxCop буду жаловаться: "реализовать IDisposable на 'Shoelace', потому что он создает члены следующих типов IDisposable: 'EventWaitHandle'."

хорошо, я реализую IDisposable on 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) и деструктор:

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, но все же убедитесь, что ваши неуправляемые объекты правильно расположены?

8 133

8 ответов:

вы не можете действительно "предотвратить" IDisposable от распространения. Некоторые классы должны быть удалены, как AutoResetEvent, и самый эффективный способ сделать это в Dispose() метод, чтобы избежать финализаторы. Но этот метод должен быть вызван каким-то образом, так же, как и в вашем примере, классы, которые инкапсулируют или содержат IDisposable, должны их утилизировать, поэтому они также должны быть одноразовыми и т. д. Единственный способ избежать этого - это:

  • избегайте использования IDisposable классов, где возможно, блокировка или ожидание событий в отдельных местах, хранение дорогостоящих ресурсов в одном месте и т. д.
  • создавайте их только тогда, когда они вам нужны, и утилизируйте их сразу после (the using шаблон)

В некоторых случаях IDisposable можно игнорировать, поскольку он поддерживает необязательный случай. Например, WaitHandle реализует IDisposable для поддержки именованного мьютекса. Если имя не используется, метод Dispose ничего не делает. MemoryStream-это еще один пример, он не использует систему ресурсы и его реализация Dispose также ничего не делает. Тщательное размышление о том, используется ли неуправляемый ресурс или нет, может быть обучающим. Так же можно изучить доступные источники для библиотек .net или использовать декомпилятор.

с точки зрения корректности вы не можете предотвратить распространение IDisposable через отношение объекта, если родительский объект создает и по существу владеет дочерним объектом, который теперь должен быть одноразовым. FxCop является правильным в этой ситуации, и родитель должен быть IDisposable.

Что вы можете сделать, это избежать добавления IDisposable к классу leaf в иерархии объектов. Это не всегда простая задача, но это интересное упражнение. С логической точки зрения, нет никаких оснований что шнурок должен быть одноразовым. Вместо того, чтобы добавлять здесь объекта waithandle, это также возможно, чтобы добавить связь между шнурком и объекта waithandle в точке использования. Самый простой способ-через экземпляр словаря.

Если вы можете переместить 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 из шнурка.

сказав это, вы можете значительно сократить одноразовый шаблон, когда у вас нет неуправляемых ресурсов. (Я все еще ищу официальную ссылку на это.)

но вы можете опустить деструктор и GC.SuppressFinalize(это); и, возможно, очистка виртуальной пустоты Утилизируйте (bool disposing) немного.

Как насчет использования инверсии управления?

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))}));
        }
    }
}