Подтвердить что ты не робот

Кто распоряжается доступной публичной собственности?

Если у меня есть класс 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.  

Помогает ли это?

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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;

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

Избегайте его в первом месте

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

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

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

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

Хранить экземпляр private
Основная проблема с размещенным кодом заключается в том, что право собственности путается. В Dispose time класс 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() в исходном экземпляре, когда он был заменен, поэтому он теперь недействителен.

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

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

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


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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

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

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

Ответ 9

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

Ответ 10

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

так:

class AContainer : IDisposable
{
    bool _isDisposed=false;

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

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