Кто распоряжается общедоступной собственностью? - PullRequest
28 голосов
/ 23 марта 2009

Если у меня есть класс SomeDisposableObject, который реализует IDisposable:

class SomeDisposableObject : IDisposable
{
    public void Dispose()
    {
        // Do some important disposal work.
    }
}

И у меня есть еще один класс с именем AContainer, который имеет экземпляр SomeDisposableObject в качестве открытого свойства:

class AContainer
{
    SomeDisposableObject m_someObject = new SomeDisposableObject();

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set { m_someObject = value; }
    }
}

Тогда FxCop будет настаивать на том, чтобы AContainer также был сделан IDisposable.

Это нормально, но я не понимаю, как можно безопасно вызвать m_someObject.Dispose() из AContainer.Dispose(), так как другой класс может все еще иметь ссылку на экземпляр m_someObject.

Каков наилучший способ избежать этого сценария?

(Предположим, что другой код полагается на AContainer.SomeObject, всегда имеющий ненулевое значение, поэтому просто перенести создание экземпляра за пределы AContainer не вариант)

Редактировать : Я добавлю несколько примеров, поскольку думаю, что некоторые комментаторы упускают эту проблему. Если я просто реализую метод Dispose() в AContainer, который вызывает m_someObject.Dispose (), то я остаюсь в следующих ситуациях:

// Example One
AContainer container1 = new AContainer();
SomeDisposableObject obj1 = container1.SomeObject;
container1.Dispose();
obj1.DoSomething(); // BAD because obj1 has been disposed by container1.

// Example Two
AContainer container2 = new AContainer();
SomeObject obj2 = new SomeObject();
container2.SomeObject = obj2; // BAD because the previous value of SomeObject not disposed.
container2.Dispose();
obj2.DoSomething(); // BAD because obj2 has been disposed by container2, which doesn't really "own" it anyway.  

Это помогает?

Ответы [ 10 ]

24 голосов
/ 23 марта 2009

Единого ответа не существует, это зависит от вашего сценария, и ключевым моментом является право собственности на одноразовый ресурс, представленный свойством, как указывает Джон Скит .

Иногда полезно взглянуть на примеры из .NET Framework. Вот три примера, которые ведут себя по-разному:

  • Контейнер всегда утилизирует . System.IO.StreamReader предоставляет одноразовое свойство BaseStream. Считается, что он является владельцем базового потока, а утилита StreamReader всегда удаляет базовый поток.

  • Контейнер никогда не утилизируется . System.DirectoryServices.DirectoryEntry предоставляет свойство Parent. Он не считается владельцем своего родителя, поэтому удаление DirectoryEntry никогда не удаляет его родителя.

    В этом случае новый экземпляр DirectoryEntry возвращается при каждом разыменовании свойства Parent, и предполагается, что вызывающий объект должен его утилизировать. Возможно, это нарушает правила для свойств, и, возможно, вместо этого должен быть метод GetParent ().

  • Контейнер иногда утилизирует . System.Data.SqlClient.SqlDataReader предоставляет одноразовое свойство Connection, но вызывающая сторона решает, владеет ли читатель (и, следовательно, удаляет) базовое соединение, используя аргумент CommandBehavior SqlCommand.ExecuteReader.

Другим интересным примером является System.DirectoryServices.DirectorySearcher, который имеет одноразовое свойство Search / Root для чтения / записи. Если это свойство установлено извне, то предполагается, что базовый ресурс не принадлежит, поэтому контейнер не удаляется. Если это не установлено снаружи, ссылка генерируется внутри, и устанавливается флаг, чтобы гарантировать, что он будет удален. Вы можете увидеть это с помощью отражателя Лутца.

Вам необходимо решить, принадлежит ли ваш контейнер ресурсу, и убедиться, что вы точно задокументировали его поведение.

Если вы решили, что владеете ресурсом, а свойство - для чтения / записи, вам необходимо убедиться, что ваш установщик располагает любой ссылкой, которую он заменяет, например ::

public SomeDisposableObject SomeObject    
{        
    get { return m_someObject; }        
    set 
    { 
        if ((m_someObject != null) && 
            (!object.ReferenceEquals(m_someObject, value))
        {
            m_someObject.Dispose();
        }
        m_someObject = value; 
    }    
}
private SomeDisposableObject m_someObject;

ОБНОВЛЕНИЕ : GrahamS справедливо указывает в комментариях, что лучше проверить m_someObject! = Значение в установщике перед удалением: я обновил приведенный выше пример, чтобы учесть это (используя вместо ReferenceEquals ! = быть явным). Хотя во многих реальных сценариях существование установщика может означать, что объект не принадлежит контейнеру и, следовательно, не будет уничтожен.

13 голосов
/ 23 марта 2009

Это действительно зависит от того, кто условно «владеет» одноразовым предметом. В некоторых случаях вы можете захотеть передать объект, например, в конструктор, без ответственности вашего класса за его очистку. В других случаях вы можете захотеть убрать это самостоятельно. Если вы создаете объект (как в вашем примере кода), то почти наверняка вы несете ответственность за его очистку.

Что касается собственности - я не думаю, что наличие собственности должно действительно передавать собственность или что-то в этом роде. Если ваш тип отвечает за утилизацию объекта, он должен сохранить эту ответственность.

5 голосов
/ 23 марта 2009

Настоящей проблемой может быть ваш объектно-ориентированный дизайн. Если AContainer удален, все его объекты-члены также должны быть удалены. Если нет, это звучит так, как будто вы можете распоряжаться телом, но хотите сохранить жизнь ногой. Не правильно звучит.

4 голосов
/ 23 марта 2009

Если в вашем классе есть одноразовый предмет, вы реализуете IDisposable с помощью метода Dispose, который утилизирует упакованные одноразовые материалы. Теперь вызывающий код должен убедиться, что используется using() или эквивалентен try / finally код, который удаляет объект.

3 голосов
/ 24 марта 2009

Я постараюсь ответить на свой вопрос:

Избегайте в первую очередь

Самый простой выход из этой ситуации - это рефакторинг кода, чтобы полностью избежать проблемы.
Есть два очевидных способа сделать это.

Создание внешнего экземпляра
Если AContainer не создает экземпляр SomeDisposableObject, а вместо этого использует внешний код для его предоставления, то AContainer больше не будет "владеть" экземпляром и не будет отвечать за его удаление.

Внешний экземпляр может быть предоставлен через конструктор или путем установки свойства.

public class AContainerClass
{
    SomeDisposableObject m_someObject; // No creation here.

    public AContainerClass(SomeDisposableObject someObject)
    {
        m_someObject = someObject;
    }

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set { m_someObject = value; }
    }
}

Хранить экземпляр в секрете
Основная проблема с размещенным кодом заключается в том, что право собственности перепутано. Во время утилизации класс AContainer не может определить, кому принадлежит экземпляр. Это может быть экземпляр, который он создал, или это может быть какой-то другой экземпляр, который был создан извне и set через свойство.

Даже если он отслеживает это и точно знает, что имеет дело с созданным им экземпляром, он все равно не может безопасно утилизировать его, поскольку другие классы теперь могут иметь ссылку на него, полученную ими. из государственной собственности.

Если код можно реорганизовать, чтобы избежать обнародования экземпляра (т. Е. Путем полного удаления свойства), тогда проблема исчезнет.

И если этого не избежать ...

Если по какой-то причине код не может быть реорганизован таким образом (как я об этом говорил в этом вопросе), то, по моему мнению, у вас остается довольно трудный выбор дизайна.

Всегда распоряжаться экземпляром
Если вы выберете этот подход, тогда вы фактически объявите, что AContainer станет владельцем экземпляра SomeDisposableObject, когда свойство будет установлено.

Это имеет смысл в некоторых ситуациях, особенно когда SomeDisposableObject явно временный или подчиненный объект. Однако это должно быть тщательно задокументировано, так как требуется, чтобы вызывающий код был осведомлен об этой передаче права собственности.

(может быть более целесообразно использовать метод, а не свойство, поскольку имя метода может использоваться для дополнительной подсказки о владении).

public class AContainerClass: IDisposable
{
    SomeDisposableObject m_someObject = new SomeDisposableObject();

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set 
        {
            if (m_someObject != null && m_someObject != value)
                m_someObject.Dispose();

            m_someObject = value;
        }
    }

    public void Dispose()
    {
        if (m_someObject != null)
            m_someObject.Dispose();

        GC.SuppressFinalize(this);
    }
}

Только утилизировать, если все еще оригинальный экземпляр
При таком подходе вы будете отслеживать, был ли экземпляр изменен с первоначально созданного AContainer, и утилизировать его только тогда, когда он был оригиналом. Здесь модель собственности смешана. AContainer остается владельцем собственного экземпляра SomeDisposableObject, но если предоставляется внешний экземпляр, то ответственность за его удаление несет внешний код.

Этот подход лучше всего отражает реальную ситуацию здесь, но его может быть трудно реализовать правильно. Код клиента все еще может вызывать проблемы, выполняя такие операции:

AContainerClass aContainer = new AContainerClass();
SomeDisposableObject originalInstance = aContainer.SomeObject;
aContainer.SomeObject = new SomeDisposableObject();
aContainer.DoSomething();
aContainer.SomeObject = originalInstance;

Здесь новый экземпляр был заменен, вызван метод, а затем восстановлен исходный экземпляр. К сожалению, AContainer будет вызывать Dispose() в исходном экземпляре при его замене, поэтому теперь он недействителен.

Просто сдавайся и пусть GC справится с этим
Это явно не идеально. Если класс SomeDisposableObject действительно содержит какой-то дефицитный ресурс, то немедленное его уничтожение определенно вызовет у вас проблемы.

Однако он также может представлять наиболее надежный подход с точки зрения взаимодействия клиентского кода с AContainer, поскольку не требует специальных знаний о том, как AContainer относится к владению экземпляром SomeDisposableObject.

Если вы знаете, что одноразовых ресурсов в вашей системе на самом деле недостаточно, то это может быть лучшим подходом.


Некоторые комментаторы предположили, что может быть возможно использовать подсчет ссылок для отслеживания, если какие-либо другие классы все еще имеют ссылку на экземпляр SomeDisposableObject. Это было бы очень полезно, поскольку позволило бы нам распоряжаться им только тогда, когда мы знаем, что это безопасно, а в противном случае просто позволяем GC справиться с этим.

Однако я не знаю ни одного C # /. NET API для определения количества ссылок на объект. Если есть, пожалуйста, дайте мне знать.

2 голосов
/ 23 марта 2009

Причина, по которой вы не можете безопасно вызывать Dispose() для AContainer экземпляра SomeDisposableObject, связана с отсутствием инкапсуляции. Публичная собственность обеспечивает неограниченный доступ к части внутреннего государства. Поскольку эта часть внутреннего состояния должна соответствовать правилам протокола IDisposable, важно убедиться, что он хорошо инкапсулирован.

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

Если вы можете избежать разоблачения своего одноразового экземпляра, проблема того, кто будет обрабатывать вызов Dispose(), также исчезнет.

1 голос
/ 24 марта 2009

Интересная вещь, с которой я столкнулся, это то, что SqlCommand обычно владеет экземпляром SqlConnection (оба реализуют IDisposable). Однако вызов dispose на SqlCommand NOT также удалит соединение.

Я обнаружил это также с помощью Stackoverflow прямо здесь .

Таким образом, другими словами, имеет значение, может ли «дочерний» (вложенный?) Экземпляр быть / будет повторно использован позже.

0 голосов
/ 24 марта 2009

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

0 голосов
/ 23 марта 2009

В общем, я думаю, что тот, кто создает объект, должен нести ответственность за утилизацию. В этом случае AContainer создает SomeDisposableObject, поэтому он должен быть уничтожен, когда AContainer имеет значение.

Если по какой-то причине вы считаете, что SomeDisposableObject должен жить дольше, чем AContainer - я могу думать только о следующих методах:

  • оставьте SomeDisposableObject нераспределенным, и в этом случае GC позаботится об этом за вас
  • дают SomeDisposableObject ссылку на AContainer (см. Элементы управления WinForms и родительские свойства). Пока SomeDisposableObject достижим, AContainer также доступен. Это помешает GC уничтожить AContainer, но если кто-то вызовет Dispose вручную - хорошо, вы удалите SomeDisposableObject. Я бы сказал, что ожидается.
  • Реализация SomeDisposableObject в качестве метода, скажем, CreateSomeDisposableObject (). Это дает понять (э), что клиент несет ответственность за утилизацию.

В целом, хотя - я не совсем уверен, что дизайн имеет смысл. В конце концов, вы, похоже, ожидаете клиентский код вроде:

SomeDisposableObject d;
using (var c = new AContainer()) {
   d = c.SomeObject;
}
// do something with d

Это похоже на сломанный клиентский код. Это нарушает Закон Деметры, и это просто здравый смысл для меня.

0 голосов
/ 23 марта 2009

Вы можете просто отметить удаление в Dispose (). В конце концов Утилизация не является деструктором - объект все еще существует.

так:

class AContainer : IDisposable
{
    bool _isDisposed=false;

    public void Dispose()
    {
        if (!_isDisposed) 
        {
           // dispose
        }
        _isDisposed=true;
    }
}

добавьте это и к вашему другому классу.

...