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

Кто должен вызывать Dispose на объекты IDisposable при передаче в другой объект?

Есть ли какие-либо рекомендации или рекомендации по поводу того, кто должен называть Dispose() на одноразовых объектах, когда они были переданы другим методам объектов или constuctor?

Вот несколько примеров того, что я имею в виду.

IDisposable object передается в метод (должен ли он избавиться от него после его завершения?):

public void DoStuff(IDisposable disposableObj)
{
    // Do something with disposableObj
    CalculateSomething(disposableObj)

    disposableObj.Dispose();
}

Идентифицируемый объект передается в метод и сохраняется ссылка (должен ли он утилизировать его при MyClass?):

public class MyClass : IDisposable
{
    private IDisposable _disposableObj = null;

    public void DoStuff(IDisposable disposableObj)
    {
        _disposableObj = disposableObj;
    }

    public void Dispose()
    {
        _disposableObj.Dispose();
    }
}

В настоящее время я думаю, что в первом примере вызывающий объект DoStuff() должен распоряжаться объектом, поскольку он, вероятно, создал объект. Но во втором примере кажется, что MyClass должен распоряжаться объектом, поскольку он держит ссылку на него. Проблема в том, что вызывающий класс может не знать, что MyClass сохранил ссылку и, следовательно, может решить избавиться от объекта до того, как MyClass закончил его использование. Существуют ли стандартные правила для такого рода сценариев? Если есть, отличаются ли они, когда одноразовый объект передается в конструктор?

4b9b3361

Ответ 1

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

Обратите внимание, что некоторые классы в платформе .NET располагают объектами, которые они получили в качестве параметров. Например, утилизация StreamReader также предоставляет базовый Stream.

Ответ 2

PS: Я отправил новый ответ (содержащий простой набор правил, которые должны называть Dispose, и как проектировать API, который имеет дело с объектами IDisposable). Хотя настоящий ответ содержит ценные идеи, я пришел к выводу, что его основное предложение часто не будет работать на практике: скрыть объекты IDisposable в "более крупнозернистых" объектах часто означает, что они должны сами стать IDisposable; поэтому каждый заканчивается там, где он запускается, и проблема остается.


Есть ли какие-либо рекомендации или рекомендации по поводу того, кто должен называть Dispose() на одноразовых объектах, когда они были переданы другим методам объектов или constuctor?

Короткий ответ:

Да, есть много советов по этой теме, и лучшее, что я знаю, это Эрик Эванс 'Агрегаты в Проект, управляемый доменами. (Проще говоря, основная идея применительно к IDisposable такова: Инкапсулируйте IDisposable в более крупнозернистом компоненте, чтобы он не видел снаружи и никогда не передавался потребителю компонента.)

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

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

Более длинный ответ — Что касается этого вопроса в более широких терминах:

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

Почему эта тема редко возникает в экосистеме .NET? Поскольку среда выполнения .NET(CLR) выполняет автоматическую сборку мусора, которая делает всю работу за вас: если вам больше не нужен объект, вы можете просто забыть об этом, и сборщик мусора в конечном итоге восстановит свою память.

Почему же возникает вопрос с объектами IDisposable? Поскольку IDisposable - все о явном, детерминированном управлении ресурсом ресурса (часто редко или дорогостоящим) ресурса: IDisposable предполагается, что объекты будут выпущены, как только они больше не нужны — и сборщик мусора - неопределенная гарантия ( "Я в конечном счете верну вам память, используемую вами!" ) просто недостаточно.

Ваш вопрос, переформулированный в более широких терминах жизни и владения объектами:

Какой объект O должен отвечать за прекращение жизни (одноразового) объекта D, который также передается объектам X,Y,Z?

Допустим несколько предположений:

  • Вызов D.Dispose() для объекта IDisposable D в основном заканчивает его время жизни.

  • Логически, срок жизни объекта может быть только один раз. (Не обращайте внимания на тот момент, когда это противоречит протоколу IDisposable, который явно разрешает множественные вызовы Dispose.)

  • Следовательно, для простоты должен быть ответственен только один объект O за размещение D. Позвоните по телефону O владельцу.

Теперь мы переходим к сути проблемы: ни язык С#, ни VB.NET не обеспечивают механизм для обеспечения отношений между объектами. Таким образом, это превращается в проблему дизайна: все объекты O,X,Y,Z, которые получают ссылку на другой объект D, должны следовать и придерживаться соглашения, которое точно регулирует, кто имеет право собственности на D.

Упростите проблему с помощью агрегатов!

Единственный лучший совет, который я нашел на эту тему, - это Эрик Эванс '2004 book, Проект, управляемый доменами. Позвольте мне привести из книги:

Предположим, что вы удаляете объект Person из базы данных. Наряду с человеком выходят имя, дата рождения и описание работы. Но как насчет адреса? Могут быть другие люди по одному и тому же адресу. Если вы удалите адрес, объекты Person будут иметь ссылки на удаленный объект. Если вы оставите его, вы накапливаете нежелательные адреса в базе данных. Автоматическая сборка мусора может устранить нежелательные адреса, но эта техническая исправление, даже если она доступна в вашей системе баз данных, игнорирует проблему базового моделирования. (стр. 125)

Посмотрите, как это относится к вашей проблеме? Адреса из этого примера эквивалентны вашим одноразовым объектам, и вопросы одинаковы: кто должен их удалить? Кто "владеет" ими?

Эванс предлагает предложить Агрегаты как решение этого проблема дизайна. Из книги снова:

Агрегат - это кластер связанных объектов, которые мы рассматриваем как единицу с целью изменения данных. Каждый Агрегат имеет корень и границу. Граница определяет, что находится внутри Агрегата. Корень представляет собой единую специфическую сущность, содержащуюся в агрегате. Корень является единственным членом Aggregate, которому внешние объекты могут содержать ссылки, хотя объекты внутри границы могут содержать ссылки друг на друга. (стр. 126-127)

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

Как насчет предположения, что создатель объекта IDisposable должен также распоряжаться им?

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

Посмотрите на два выдающихся типа интерфейса из библиотеки базового класса .NET(BCL), которые явно нарушают это правило: IEnumerable<T> и IObservable<T>. Оба являются по существу фабриками, которые возвращают объекты IDisposable:

  • IEnumerator<T> IEnumerable<T>.GetEnumerator()
    (Помните, что IEnumerator<T> наследуется от IDisposable.)

  • IDisposable IObservable<T>.Subscribe(IObserver<T> observer)

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

Кстати, этот пример также демонстрирует пределы совокупного решения, описанного выше: как IEnumerable<T>, так и IObservable<T> являются слишком общими по своей природе, чтобы быть частью совокупности. Агрегаты обычно очень специфичны для домена.

Дополнительные ресурсы и идеи:

  • В UML "есть" отношения между объектами могут быть смоделированы двумя способами: как агрегация (пустой алмаз), так и как состав (заполненный алмаз). Композиция отличается от агрегации тем, что срок жизни объекта, находящегося в указанном объекте, заканчивается таковым для контейнера/реферера. Ваш исходный вопрос подразумевает агрегацию ( "переносимое право собственности" ), в то время как я в основном ориентирован на решения, которые используют состав ( "фиксированное владение" ). См. Статья в Википедии "Состав объекта" .

  • Autofac (.NET IoCконтейнер) решает эту проблему двумя способами: либо путем общения, используя так называемый тип отношения, Owned<T>, который приобретает право собственности на IDisposable; или через концепцию единиц работы, называемых областями времени жизни в Autofac.

  • Относительно последнего, Николас Блумхардт, создатель Autofac, написал "Autofac Lifetime Primer" , который включает раздел "IDisposable and ownership". Вся статья - отличный трактат о проблемах владения и жизни в .NET. Я рекомендую прочитать его, даже тем, кто не интересуется Autofac.

  • В С++ Инициализация ресурсов (RAII) идиома (в общем) и типы интеллектуальных указателей (в частности) помогают программисту правильно получить срок жизни объекта и права собственности. К сожалению, они не переносятся на .NET, потому что .NET не имеет элегантной поддержки С++ для детерминированного уничтожения объекта.

  • См. также этот ответ на вопрос о переполнении стека, "Как учитывать разрозненные потребности в реализации?" , который (если я правильно понимаю) следует аналогичной мысли, как мой ответ на основе Aggregate: построение крупнозернистого компонента вокруг IDisposable, так что он полностью содержится (и скрыт от потребителя компонента) внутри.

Ответ 3

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

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

Нет автоматического ответа, в котором говорится "Да, всегда удаляйте" или "Нет, никогда не удаляйте", когда говорите о данных члена. Скорее, вам нужно подумать об объектах в каждом конкретном случае и спросить себя: "Является ли этот объект ответственным за время жизни одноразового объекта?"

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

public class Foo
{
    public MyClass BuildClass()
    {
        var dispObj = new DisposableObj();
        var retVal = new MyClass(dispObj);
        return retVal;
    }
}

Foo явно отвечает за создание dispObj, но передаёт право собственности на экземпляр MyClass.

Ответ 4

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

В моем предыдущем ответе было одно право: Каждый IDisposable должен иметь эксклюзивного "владельца", который будет отвечать за Dispose -инг его ровно один раз. Управление IDisposable объектами, тогда становится очень сопоставимым с управлением памятью в неуправляемых сценариях кода.

Технология .NET-предшественника, Component Object Model (COM), использовала следующую > для управления памятью между объектами:

  • "В параметрах должны быть выделены и освобождены вызывающим абонентом.
  • " Out-parameters должны быть выделены вызываемым, они освобождаются вызывающим абонентом [...].
  • "Ввод-параметры первоначально выделяются вызывающим абонентом, а затем освобождаются и перераспределяются вызываемым, если это необходимо. Как верно для параметров out, вызывающий отвечает за освобождение окончательного возвращаемого значения".

(Существуют дополнительные правила для случаев ошибок, см. страницу, связанную с приведенным выше описанием.)

Если бы мы адаптировали эти рекомендации для IDisposable, мы могли бы установить следующее...

Правила относительно IDisposable собственности:

  • Когда IDisposable передается в метод с помощью регулярного параметра, передача права собственности отсутствует. Вызываемый метод может использовать IDisposable, но не должен Dispose его (и не передавать права собственности, см. Правило 4 ниже).
  • Когда IDisposable возвращается из метода через параметр out или возвращаемое значение, тогда собственность передается от метода к его вызывающему. Вызывающий должен будет Dispose его (или передать права собственности на IDisposable таким же образом).
  • Когда IDisposable присваивается методу с помощью параметра ref, то владение над ним переносится на этот метод. Метод должен скопировать IDisposable в локальное поле переменной или объекта, а затем установить для параметра ref значение null.

Из вышеизложенного следует одно важное правило:

  1. Если у вас нет собственности, вы не должны ее передавать. Это означает, что если вы получили объект IDisposable через обычный параметр, не помещайте один и тот же объект в параметр ref IDisposable и не подвергайте его с помощью возвращаемого значения или параметра out.

Пример:

sealed class LineReader : IDisposable
{
    public static LineReader Create(Stream stream)
    {
        return new LineReader(stream, ownsStream: false);
    }

    public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
    {
        try     { return new LineReader(stream, ownsStream: true); }
        finally { stream = null;                                   }
    }

    private LineReader(Stream stream, bool ownsStream)
    {
        this.stream = stream;
        this.ownsStream = ownsStream;
    }

    private Stream stream; // note: must not be exposed via property, because of rule (2)
    private bool ownsStream;

    public void Dispose()
    {
        if (ownsStream)
        {
            stream?.Dispose();
        }
    }

    public bool TryReadLine(out string line)
    {
        throw new NotImplementedException(); // read one text line from `stream` 
    }
}

Этот класс имеет два статических метода factory и, таким образом, позволяет своему клиенту выбирать, хочет ли он сохранить или передать права собственности:

  • Один принимает объект Stream через обычный параметр. Это сигнализирует вызывающему, что собственность не будет принята. Таким образом, вызывающему абоненту необходимо Dispose:

    using (var stream = File.OpenRead("Foo.txt"))
    using (var reader = LineReader.Create(stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    
  • Тот, кто принимает объект Stream через параметр ref. Это сигнализирует вызывающему, что право собственности будет передано, поэтому вызывающему абоненту не требуется Dispose:

    var stream = File.OpenRead("Foo.txt");
    using (var reader = LineReader.Create(ref stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    

    Интересно, что если Stream были объявлены как переменная using: using (var stream = …), компиляция завершится с ошибкой, потому что переменные using не могут быть переданы как параметры ref, поэтому компилятор С# помогает обеспечить соблюдение наших правил в этом конкретном случай.

Наконец, обратите внимание, что File.OpenRead является примером для метода, который возвращает объект IDisposable (а именно, Stream) через возвращаемое значение, поэтому владение над возвращенным потоком передается вызывающему.

Недостатки:

Основным недостатком этого шаблона является то, что AFAIK, никто его не использует (пока). Поэтому, если вы взаимодействуете с любым API, который не соответствует приведенным выше правилам (например, Библиотека базового класса .NET Framework), вам все равно нужно прочитать документацию, чтобы узнать, кому нужно называть Dispose на объектах IDisposable.

Ответ 5

Одна вещь, которую я решил сделать, прежде чем я много знал о .NET-программировании, но она по-прежнему кажется хорошей идеей, имеет конструктор, который принимает IDisposable, также принимает логическое выражение, которое говорит, будет ли принадлежность объекта к также переносится. Для объектов, которые могут существовать полностью в рамках операторов using, это, как правило, не будет слишком важным (поскольку внешний объект будет располагаться в пределах объекта Inner object Using block, нет необходимости в том, чтобы внешний объект располагался внутренний, действительно, может быть необходимо, чтобы он этого не делал). Однако такая семантика может стать существенной, когда внешний объект будет передан как интерфейс или базовый класс для кода, который не знает о существовании внутреннего объекта. В этом случае предполагается, что внутренний объект будет жить до тех пор, пока внешний объект не будет уничтожен, и предмет, который знает, что внутренний объект должен умереть, когда внешний объект делает это сам внешний объект, поэтому внешний объект должен быть способен уничтожить внутренний.

С тех пор у меня было несколько дополнительных идей, но я их не пробовал. Мне было бы любопытно, что думают другие люди:

  • Обертка для подсчета ссылок для объекта IDisposable. Я действительно не понял наиболее естественный шаблон для этого, но если объект использует подсчет ссылок с блокировкой приращения/декремента и если (1) весь код, который манипулирует объектом, использует его правильно, и (2) нет циклических ссылок создаются с использованием объекта, я бы ожидал, что у него должен быть общий объект IDisposable, который будет уничтожен, когда последнее использование будет показываться. Вероятно, должно произойти, что публичный класс должен быть оболочкой для частного класса с подсчетом ссылок, и он должен поддерживать метод конструктора или factory, который создаст новую оболочку для одного и того же базового экземпляра (наброса счетчика ссылок экземпляра одним). Или, если класс нужно очистить, даже когда оболочки закрыты, и если класс имеет некоторую периодическую процедуру опроса, класс может содержать список WeakReference для своих оберток и проверять, чтобы по крайней мере некоторые из них по-прежнему существуют.
  • Если конструктор для объекта IDisposable принимает делегата, который он вызывается при первом удалении объекта (объект IDisposable должен использовать Interlocked.Exchange для флага isDisposed, чтобы обеспечить его расположение ровно один раз). Этот делегат мог бы позаботиться об утилизации любых вложенных объектов (возможно, с проверкой, чтобы проверить, все ли еще держат их).

Кто-нибудь из них кажется хорошим шаблоном?