Кто распоряжается идентифицируемой общественной собственностью?

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

  •  21-08-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

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

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

Другим интересным примером является System.DirectoryServices.DirectorySearcher, который имеет доступное для чтения / записи свойство 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;

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

итак:

class AContainer : IDisposable
{
    bool _isDisposed=false;

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top