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

Но есть множество вариантов владения, которые не являются персональной ответственностью объекта:
  • Ресурсы, которыми владеют зависимости. При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами, зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее. Кстати, этот довод сразу ставит крест на любых бизнес-интерфейсах, расширяющих IDisposable: такой интерфейс требует от своих реализаций невозможного — отвечать за себя и за того парня (зависимости)
  • Ресурсы, которые при некоторых условиях не надо очищать. Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose
  • Ресурсы, которые являются внешними по отношению к зависимости, но требуются клиенту в процессе ее использования. Самый простой пример — подписка на события объекта при присвоении его свойству.


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

Новый IDisposable<T>: теперь с обобщением


    public interface IDisposable<out T> : IDisposable
    {
        T Value { get; }
    }

Семантика обобщенного IDisposable отличается от обычного примерно так же как «можете быть свободны» от «немедленно освободите помещение». Теперь очистка ресурсов отделена от реализации основной функциональности и может определяться как поставщиком зависимости, так и ее потребителем.

Реализация проста как мычание:
    public class Disposable<T> : IDisposable<T>
    {
        public Disposable(T value, IDisposable lifetime)
        {
            _lifetime = lifetime;
            Value = value;
        }
   
        public void Dispose()
        {
            _lifetime.Dispose();
        }

        public T Value { get; }

        private readonly IDisposable _lifetime;
    }


Используем стероиды


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

Для начала избавим себя от вызова конструктора с явным указанием типа с помощью метода расширения:
        public static IDisposable<T> ToDisposable<T>(this T value, IDisposable lifetime)
        {
            return new Disposable<T>(value, lifetime);
        }

Для использования достаточно просто написать:
        var disposableResource = resource.ToDisposable(disposable);

Типы компилятор в львиной доле случаев успешно выведет сам.

Если объект уже наследует IDisposable и эта реализация нас устраивает, то можно и без аргументов:
        public static IDisposable<T> ToSelfDisposable<T>(this T value) where T : IDisposable
        {
            return value.ToDisposable(value);
        }

Если ничего удалять не надо, но от нас ждут, что мы умеем (помните про вредный StreamReader?):
        public static IDisposable<T> ToEmptyDisposable<T>(this T value) where T : IDisposable
        {
            return value.ToDisposable(Disposable.Empty);
        }

Если хочется автоматически отписаться от событий объекта при расставании:
        public static IDisposable<T> ToDisposable<T>(this T value, Func<T, IDisposable> lifetimeFactory)
        {
            return value.ToDisposable(lifetimeFactory(value));
        }

… и применять вот так:
        var disposableResource = new Resource().ToDisposable(r => r.Changed.Subscribe(Handler));

Если очистка требует выполнения специального кода, то и здесь на помощь придет однострочник:
        public static IDisposable<T> ToDisposable<T>(this T value, Action<T> dispose)
        {
            return value.ToDisposable(value, Disposable.Create(() => dispose(value)));
        }

И даже если специальный код также нужен для инициализации:
        public static IDisposable<T> ToDisposable<T>(this T value, Func<T, Action> disposeFactory)
        {
            return new Disposable<T>(value, Disposable.Create(disposeFactory(resource)));
        }

Использовать еще проще чем рассказывать:
        var disposableViewModel = new ViewModel().ToDisposable(vm => 
        {
            observableCollection.Add(vm);
            return () => observableCollection.Remove(vm);
        });

А что если у нас уже есть готовая обертка, но надо добавить к ней еще немного ответственности за очистку ресурсов?
Нет проблем:
        public static IDisposable<T> Add<T>(this IDisposable<T> disposable, IDisposable lifetime)
        {
            return disposable.Value.ToDisposable(Disposable.Create(disposable, lifetime));
        }


Итоги


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

Что удивительно, несмотря на наличие как минимум одного полного аналога IDisposable<T> в лице Owned<T> из Autofac, беглое гугление не выявило похожих методов расширения.

Надеюсь, статья и применение ее материалов на практике доставит читателям не меньшее удовольствие, чем автору.
Любые дополнения и критика приветствуются.

Комментарии (110)


  1. FiresShadow
    14.12.2015 07:30
    +2

    Не совсем понял мотивацию для использования.

    При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее.
    У класса есть ссылка на зависимость, по этой ссылке можно вызвать Dispose.

    class Foo : IDisposable
    {
        IDependency _dependency;
    
        public Foo(IDependency dependency)
        {
            _dependency = dependency;
        }
    
        public void Dispose()
        {
            if (_dependency is IDisposable)
                ((IDisposable)_dependency).Dispose();
        }
    }
    
    

    Зависимостями зависимости может управлять сама зависимость. Логику освобождения ресурсов можно поместить в Dispose конкретного класса. Если логика освобождения ресурсов может различаться в разных ситуациях, можно использовать стратегию + фабрику.

    В вашем примере логика освобождения ресурсов размазывается по нескольким классам. Часть логики в методе ToDisposable() одного класса (
    return value.ToDisposable(Disposable.Empty);return value.ToDisposable(value);), часть логики — непосредственно в Dispose конкретного класса (к которому принадлежит value). Для чего это и в чём выигрыш?


    1. mird
      14.12.2015 09:08
      +5

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


      1. FiresShadow
        14.12.2015 09:21

        В случае, когда логика освобождения ресурсов может различаться в разных ситуациях, я предложил использовать стратегию. Нужно освободить разделяемый ресурс — освобождаем, не нужно — не освобождаем. Лучше увидеть внутри Dispose применение стратегии, чем увидеть внутри Dispose часть логики освобождения ресурсов (как предлагает автор статьи), а потом разбираться, почему освобождение ресурсов работает совсем не так, как описано в Dispose, и как оно работает на самом деле.

        лучше вбрасывать фабрику и использовать ее
        Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?


        1. mird
          14.12.2015 10:14
          +1

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

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

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

          Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?


          Если вы работаете с DI контейнером, можно сделать так:

          class Foo : IDisposable
          {
              Func<IDependency> _dependencyFactory;
          
              public Foo(Func<IDependency> dependencyFactory)
              {
                  _dependencyFactory= dependencyFactory;
              }
          
              public void SomMethod()
              {
                    ...
                   using(var dependency = _dependencyFactory())
                   {
                        ///используем зависимость.
                   }
              }
          }
          


          1. Bonart
            14.12.2015 10:25

            Предлогаемые им обертки ничего не упрощают, при этом усложняя код или компиляцию.

            Вам же не составит труда привести примеры усложнения в сравнении с более простыми аналогами?
            Если вы работаете с DI контейнером, можно сделать так

            Нельзя сделать так.
            1. IDependency не обязан наследоваться от IDisposable
            2. Экземпляр реализации IDependency может переиспользоваться (пример — соединения)
            3. У реализации IDependency могут быть собственные зависимости

            Так что корректно определить сигнатуру фабрики можно вот так (Autofac)
            Func<Owned<IDependency>> _dependencyFactory;
            

            … или вот так:
            Func<IDisposable<IDependency>> _dependencyFactory;
            


            1. lair
              14.12.2015 11:06
              +1

              Я, на всякий случай, замечу, что есть разница между Owned<T> и любым другим классом (если только вы не написали свое расширение к Autofac): когда вы сделаете Dispose на Owned<T>, Autofac закроет соответствующий LifetimeScope, и все зависимости, которые он создавал под T, будут явно отпущены.


              1. Bonart
                14.12.2015 11:53

                С точки зрения классов, реализующих функциональность, разница между Owned и IDisposable<T> только в том, что первый требует ссылку на сборку с Autofac. Семантика абсолютно одинаковая.
                А с точки зрения реализации Composition Root вы правы: Autofac реализует именно такое поведение для Owned по умолчанию. Впрочем, его легко переопределить и чуть сложнее реализовать аналогичное для IDisposable<T>


                1. lair
                  14.12.2015 13:39
                  +1

                  Семантика абсолютно одинаковая.

                  Да разве? А мне казалось, что семантика IDisposable<T> полностью определяется тем, кто его создает, и может не делать вообще ничего.


                  1. Bonart
                    14.12.2015 13:58

                    Для того, кто ресурсами пользуется, семантика одинаковая: «Мне этот ресурс больше не нужен, можете делать с ним все, что считаете нужным».


                    1. mird
                      14.12.2015 14:11
                      +1

                      Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.


                      1. Bonart
                        14.12.2015 14:26
                        +1

                        Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.

                        Вы правда прочитали статью?
                        Там прямым текстом указаны различия в семантике IDisposable и IDisposable<T>
                        Семантика обобщенного IDisposable отличается от обычного примерно так же как «можете быть свободны» от «немедленно освободите помещение».


                        1. mird
                          14.12.2015 14:43
                          +1

                          Так вот вопрос, зачем вы называете свой интерфейс так же как уже существующий, с устоявшейся семантикой? Чтобы было проще?


                          1. Bonart
                            14.12.2015 16:01
                            -1

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


                            1. Serg046
                              14.12.2015 19:10
                              +2

                              Ну ведь правда же легко спутать. Я от того даже плюсанул.
                              Ну, в этом случае release звучит более корректно, чем dispose.


                    1. lair
                      14.12.2015 14:15

                      Вот только у Owned семантика другая, и она означает: закройте lifetime scope, который был открыт при создании Owned. Это, только это, и ничего, кроме этого.


                      1. Bonart
                        14.12.2015 14:31

                        А вот здесь вы неправы.

                        закройте lifetime scope, который был открыт при создании Owned

                        Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
                        Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.
                        Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.


                        1. lair
                          14.12.2015 14:35
                          +2

                          Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
                          Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.

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

                          Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.

                          Тем не менее, в Autofac она такова. Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки. И в любом случае, это не решение для частого использования.


                          1. Bonart
                            14.12.2015 16:12

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

                            Неужели? Вот что написано по вашей же ссылке об ожиданиях клиентского кода:
                            An owned dependency can be released by the owner when it is no longer required. Owned dependencies usually correspond to some unit of work performed by the dependent component.

                            Сразу после — пример того самого клиентского кода.
                            И только потом — объяснение, как оно работает по умолчанию.
                            Пока клиентский код исполняет контракт «can be released by the owner when it is no longer required» никаких проблем с нарушением ожиданий нет и не будет.
                            Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки.

                            Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.


                            1. lair
                              14.12.2015 16:47
                              +1

                              И только потом — объяснение, как оно работает по умолчанию.

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

                              Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.

                              Зато привязывает к вашему инструменту, и имеет неопределенную семантику.


                              1. Bonart
                                14.12.2015 17:00

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

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

                                Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает? Ну я даже не знаю.
                                имеет неопределенную семантику

                                Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.


                                1. lair
                                  14.12.2015 17:04

                                  Полагаю, что напрасно. Такая трактовка вас же и ограничивает, при этом не давая никаких плюшек взамен.

                                  Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.

                                  Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает?

                                  Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.

                                  Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.

                                  Это минус как раз. Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.


                                  1. Bonart
                                    14.12.2015 17:22

                                    Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.

                                    Где вы нашли неопределенность? На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю». Напротив, семантика определена весьма жестко и компактно, соблюдать такой контракт проблем нет c обеих сторон.
                                    Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.

                                    Это только когда моя статья превратиться в готовый к установке nuget-пакет. Но даже тогда зависимость будет не столь неприятной, как от контейнера, которому место строго в Composition Root.
                                    Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.

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


                                    1. lair
                                      14.12.2015 17:36
                                      +1

                                      Где вы нашли неопределенность?

                                      Там, где вы выше написали, что неопределенная семантика — это благо.

                                      На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю».

                                      А такой контракт мне просто не нужен, он не решает моих задач.

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

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


            1. mird
              14.12.2015 11:59

              Вы в этой статье написали некую реализацию

              IDisposable<T>
              которая является оберткой вокруг IDisposable. При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».
              Нельзя сделать так.
              IDependency не обязан наследоваться от IDisposable
              Экземпляр реализации IDependency может переиспользоваться (пример — соединения)
              У реализации IDependency могут быть собственные зависимости


              Если бы вы не вырывали из контекста, вы бы увидели, что пример кода там был в случае, если хочется больше контроля над disposable зависимостями. А значит данная конкретная зависимость наследуется от IDisposable. Наиболее правильно (и об этом я пишу выше), передать управление зависимостями DI контейнеру.


              1. Bonart
                14.12.2015 12:24

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

                То есть вы не готовы показать усложнение кода или компиляции на примере? Очень жаль, без них ваша оценка сильно теряет в убедительности.
                Кстати, Николас Блумхард тоже почему-то с вами не согласен.
                А значит данная конкретная зависимость наследуется от IDisposable

                Вот именно что НЕ значит. Не может зависимость отвечать за свои зависимости и так далее, а вот Composition Root может и должен. Но есть нюанс: о том, что с момента X зависимость Y больше не нужна объекту Z этот самый объект должен как-то сообщить. Мой IDisposable<T>, так же как и Owned из Autofac, именно эту задачу и решает.


                1. mird
                  14.12.2015 13:02

                  Если эта зависимость не диспозабл, а внутри нее какие-то диспозабл зависимости, то этот код должен переехать глубже (к диспозабл зависимостям).
                  Я понимаю что такое Owned из Autofac, и его использование, что характерно, оверхеда почти не добавляет (потому что весь оверхед в том, что в фабрику мне нужно добавить слово Owned). При этом я делегирую управление зависимостью Autofac. Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?


                  1. Bonart
                    14.12.2015 14:19
                    -1

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

                    Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).
                    Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?

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

                    Решение:

                    Давать клиентам зависимость в виде
                    Func<IDisposable<IConnection>>
                    

                    Клиенты получают соединение, пользуются им, после чего вызывают Dispose у IDisposable<IConnection>

                    Реализовать «фабрику» соединений можно так:
                    () => context =>
                    {
                        var pool = context.Resolve<IConnectionPool>();
                        return pool.GetConnection().ToDisposable(connection => pool.PutConnection(connection));
                    }
                    


                    1. lair
                      14.12.2015 14:39
                      +2

                      По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).

                      (не Dependency Injection, а Dependency Inversion, в данном случае, но не принципиально).

                      На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.

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

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


                      1. Bonart
                        14.12.2015 16:18

                        На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.

                        А где навязывание? Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.
                        Вы же не отвергаете по тому же принципу обобщенные коллекции?
                        Ну и типовой коннекшн пул, прекрасно реализуется без дополнительных оберток. Более того, реализуется прозрачно для клиента.

                        Пример прозрачной для клиента реализации можете привести?
                        Кстати, у меня само соединение о пуле тоже ничего не знает.


                        1. lair
                          14.12.2015 16:48

                          Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.

                          Ну так странно же комбинировать Dispose с объектом, у которого его нет.

                          Пример прозрачной для клиента реализации можете привести?

                          SqlConnection.


                          1. Bonart
                            14.12.2015 18:21

                            Ну так странно же комбинировать Dispose с объектом, у которого его нет.

                            Не более странно чем комбинировать одиночные объекты в коллекции.
                            SqlConnection

                            Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.


                            1. lair
                              14.12.2015 18:26

                              Не более странно чем комбинировать одиночные объекты в коллекции.

                              Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.

                              Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.

                              Вы просили простую для клиента. И для клиента проще придумать сложно.


                              1. Bonart
                                15.12.2015 13:58

                                Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.

                                Как и обязанности одиночной реализации от заворачивания в IDisposable<T>
                                Вы просили простую для клиента. И для клиента проще придумать сложно.

                                Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное? Видимо, не так уж оно и прекрасно на практике-то?
                                Кстати, я для клиента проще не только придумал, но и реализовал. Клиент вообще не имел дела с соединениями, только с транзакциями.


                                1. lair
                                  15.12.2015 14:26

                                  Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное?

                                  А какая разница, свое оно или чужое? Мне достаточно того, что оно работает с заданной семантикой.

                                  Кстати, я для клиента проще не только придумал, но и реализовал

                                  Пример кода в студию. Только именно кода connection (object) pool, потому что как убрать от пользователя коннекшны, я как раз прекрасно знаю, только здесь это не обсуждалось. А то мне как-то сложно себе представить что-то проще чем

                                  using(var cn = new SqlConnection(...))
                                  {
                                    cn.Open();
                                    //...
                                  }
                                  


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


                    1. mird
                      14.12.2015 14:47

                      Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).

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

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


                      Про коннекшн пул не слышали? Типовой паттерн.


                      1. Bonart
                        14.12.2015 16:22

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

                        Либо вы так и не прочитали статью.
                        Зависимости нельзя диспозить, так как за их время жизни отвечает Composition Root, а не клиент.
                        Зависимости надо диспозить, так ненужные дорогие ресурсы надо освобождать как можно быстрее, но про момент ненужности знает только клиент, а не Composition Root.
                        Для разрешения этого противоречия и был придуман IDisposable<T>


                      1. Bonart
                        14.12.2015 16:28

                        Про коннекшн пул не слышали? Типовой паттерн.

                        Не только слышал.
                        Вы точно прочитали мой комментарий? Там как раз про реализацию пула, прозрачную как для клиента, так и для самого соединения.


                        1. mird
                          14.12.2015 17:38

                          А зачем мне ваша обертка над коннекшн пулом?


    1. Bonart
      14.12.2015 09:30
      +1

      Нельзя у зависимости вызвать Dispose: класс-клиент ей не владеет.

      1. Одна зависимость может разделяться несколькими клиентами и за это отвечает не клиент, а CompositionRoot
      2. Зависимость сама может не быть IDisposable, в отличие от ее собственных зависимостей, зависимостей ее зависимостей и т.д.


      1. FiresShadow
        14.12.2015 10:06
        -1

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


        1. mird
          14.12.2015 10:17
          +4

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


          1. creker
            14.12.2015 11:25
            +1

            Поэтому у StreamWriter есть параметр в конструкторе, чтобы стрим извне не освобождался при Dispose у StreamWriter. И то и то поведение нужное и полезное, поэтому дан выбор. Не понятно, к чему пример конкретно с ним.


            1. Bonart
              14.12.2015 12:01

              Ответил про выбор поведения в другом комментарии


  1. FiresShadow
    14.12.2015 08:35

    var disposableViewModel = new ViewModel().ToDisposable(vm => 
            {
                observableCollection.Add(vm);
                return () => observableCollection.Remove(vm);
            });
    
    Тут можно было просто добавить событие\делегат в ViewModel, тогда cohesion было бы выше при том же coupling.


    1. Bonart
      14.12.2015 09:34

      Куда именно добавить? Из конструктора события вызывать бесполезно, IDisposable viewModel реализовывать не обязана, как и знать о своем нахождении в ObservableCollection.


      1. FiresShadow
        14.12.2015 09:52

        Не вижу никаких препятствий, мешающих ViewModel реализовать IDisposable

            class ViewModel : IDisposable
            {
                public Action<ViewModel> DisposeStrategy;
                
                public void Dispose()
                {
                    /*тут освобождение своих ресурсов*/
        
                    try
                    {
                        if (DisposeStrategy != null)
                            DisposeStrategy(this);
                    }
                    finally
                    {
                        DisposeStrategy = null;
                    }
                }
            }
        


        var viewModel = new ViewModel() {DisposeStrategy = vm => observableCollection.Remove(vm)};
        observableCollection.Add(viewModel);
        


        1. Bonart
          14.12.2015 10:09

          1. Вы добавили внутрь ViewModel ответственность, которая ей самой не нужна
          2. Вы исключили сценарий дальнейшего переиспользования конкретного экземпляра ViewModel
          3. Вам потребуется более трудоемкий рефакторинг при замене типа ViewModel на другой класс
          4. Ваш код еще не делает того, что делает мой, но уже больше и сложнее


          1. FiresShadow
            14.12.2015 11:22
            +1

            Допустим, у вас в ViewModel «на одну ответственность меньше», а сколько тогда ответственностей у Disposable[ViewModel]? А если понадобится добавить функцию печати и импорта в Excel, вы напишите Printable<Excelable<Disposable[ViewModel]>>?


  1. lair
    14.12.2015 11:00
    +3

    зависимость может реализовать IDisposable, а может не реализовать

    Эээ, если зависимость не реализует IDisposable, то в чем проблема-то? Не реализует — не диспозь.


    1. Bonart
      14.12.2015 11:48
      -4

      Так у нее свои зависимости могут быть, вполне себе диспозабельные. И если исходная зависимость с какого-то времени нам не нужна, то надо об этом как-то сообщить, чтобы CompositionRoot смог очистить все, что уже не требуется никому.
      Owned в Autofac именно для этого.


      1. lair
        14.12.2015 13:38
        +2

        Кому сообщить? DI-контейнеру? Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке), а не IDisposable, семантика-то разная совершенно.


        1. Bonart
          14.12.2015 14:21

          семантика-то разная совершенно.

          Чем она разная-то? И там, и там «это мне больше не нужно».
          Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке)

          … и пронесем зависимость от контейнера в обычный класс?


          1. lair
            14.12.2015 14:32

            Чем она разная-то? И там, и там «это мне больше не нужно».

            Да нет же. IDisposable означает «отпусти ресурсы». Owned означает «закрой lifetime scope». Упомянутый выше псевдоинтерфейс означает «передай контейнеру, что я все, пусть делает, что хочет».

            … и пронесем зависимость от контейнера в обычный класс?

            А так пронесем зависимость от вашего IDisposable<T> — оно чем-то лучше?

            Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.


            1. Bonart
              14.12.2015 16:42

              А так пронесем зависимость от вашего IDisposable — оно чем-то лучше?
              Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
              Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.

              Вы можете так считать. Я полагаю, что поддержка абстракции классов-клиентов от контейнеров — один из типовых вариантов использования для IDisposable<T> Просто в Autofac эта тема на мой взгляд раскрыта настолько полно, что я предпочел ограничиться ссылкой.


              1. lair
                14.12.2015 16:49

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

                Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом. И именно поэтому оно мне низачем не надо.


                1. Bonart
                  14.12.2015 17:06

                  Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом.

                  Не понял — CompositionRoot реализуете не вы, а кто-то другой? Если вы, то нет никакой проблемы реализовать любое нужное вам поведение.
                  Кроме того, а какие обещания вам нужны на стороне класса-клиента? Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?


                  1. lair
                    14.12.2015 17:10

                    CompositionRoot реализуете не вы, а кто-то другой?

                    Не я.

                    Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?

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

                    А сообщать о том, что ресурс мне больше не нужен, мне, как разработчику класса-клиента, низачем не сдалось.


                    1. Bonart
                      14.12.2015 18:30

                      Так вот, я запрашиваю зависимость

                      Это у вас уже не Dependency Injection, а Service Locator какой-то
                      Так вот, я запрашиваю зависимость, и я хочу быть уверен, что (а) эта зависимость (вместе с деревом зависимостей) будет порезолвлена именно тогда, когда я попрошу, и (б) эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.

                      Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak. В таком варианте IDisposable<T> вам и в самом деле ни к чему


                      1. lair
                        14.12.2015 18:39
                        +1

                        Это у вас уже не Dependency Injection, а Service Locator какой-то

                        Нет. Параметр в конструкторе (в случае использования Dependency Injection) имеет семантику «для моей работы нужна вот такая зависимость — дай мне ее».

                        Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak.

                        Не больше, чем ваш IDisposable<T>. Впрочем нет, в терминах Симана это не Control Freak, потому что «The CONTROL FREAK anti-pattern occurs every time we get an instance of a DEPENDENCY by directly or indirectly using the new keyword in any place other than a COMPOSITION ROOT». К явному управлению жизненным циклом это отношения не имеет.


                        1. Bonart
                          15.12.2015 13:26

                          К явному управлению жизненным циклом это отношения не имеет

                          В таком случае IDisposable<T> полностью соответствует заявленным вами требованиям.
                          Вызов Func<Disposable<T>> именно что создает зависимость для вашего объекта, а вызов IDisposable<T>.Dispose() ее отпускает. При этом создание и отпускание зависимости вовсе не означает обязательное конструирование или очистку объектов-реализаций.


                          1. lair
                            15.12.2015 14:23

                            Не соответствует. Я хочу управление lifetime scope. А поведение IDisposable<T>.Dispose(), как уже обсуждалось, не определено.


                            1. Bonart
                              15.12.2015 15:22

                              Поведение, что того, что другого определяется Compositon Root.
                              С вашей стороны есть только уведомление Composition Root о разрыве зависимости и не более того.
                              Реальное поведение зависит от реализации Composition Root (конфигурации контейнера) и никакая lifetimeScope определенности по факту не добавляет. Я могу сконфигурировать контейнер так, что при очистке lifetimeScope будет выполнен произвольный код.
                              Хотите узнать, что точно будет — смотрите код Composition Root и никак иначе.


                              1. lair
                                15.12.2015 16:03

                                Поведение, что того, что другого определяется Compositon Root.

                                Поведение Owned описано в документации на Autofac. Composition Root, который его переопределяет (я такого не знаю ни одного) — радикально не прав.


                    1. Bonart
                      15.12.2015 13:46

                      >CompositionRoot реализуете не вы, а кто-то другой?
                      Не я.

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

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

                      «Зависимость отпущена» — это и есть всего лишь информация для Composition Root, что вы от ресурса больше не зависите, т.е. он вам не нужен.
                      Будут ли при разрешении зависимости создаваться какие-то объекты, а при отпускании — очищаться, вы на уровне клиента не можете гарантировать никак. Это ответственность Composition Root, а клиент может только сообщить «Мне нужно X и получить желаемое» и «мне больше не нужно X».


                      1. lair
                        15.12.2015 14:28

                        Тогда у вас никаких гарантий по определению

                        … кроме тех, которые предоставляет контейнер.

                        Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует.

                        Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.


                        1. Bonart
                          15.12.2015 15:26

                          … кроме тех, которые предоставляет контейнер.

                          А в контейнере все определяется его конфигурацией. Тут хоть совой об пень, хоть пнем об сову.
                          Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.

                          Во-первых даже это не гарантировано — поведение по умолчанию вполне возможно переопределить, причем не хаками, а штатными средствами. Во-вторых результат создания-очистки скоупа также определяется конфигурацией контейнера. Контейнер — это всего лишь инструмент для реализации Composition Root он специально сделан максимально гибким.


                          1. lair
                            15.12.2015 16:04

                            А в контейнере все определяется его конфигурацией.

                            Ну не все. Какие-то вещи захардкожены. И те, кто переопределяет очевидное поведение, сами за это отвечают.


                            1. Bonart
                              15.12.2015 16:59

                              Захардкожено как раз следование конфигурации, то бишь тотальный софткод :)


                              1. lair
                                15.12.2015 17:00

                                Вы хоть в реализацию Owned смотрели, прежде, чем такое писать?


                                1. Bonart
                                  15.12.2015 17:22

                                  Я-то как раз смотрел.
                                  И мне не проблема сконфигурировать контейнер так, чтобы вообще не трогая все, что Owned-based, тем не менее добиться исполнения произвольного кода при очистке Owned-скоупа.


                                  1. lair
                                    15.12.2015 17:25

                                    Мне тоже не проблема. Но вы тем самым нарушаете поведение, описанное в документации.


                                    1. Bonart
                                      15.12.2015 17:48

                                      Не нарушаю, специально же написал:

                                      вообще не трогая все, что Owned-based


                                      1. lair
                                        15.12.2015 17:50

                                        Окей, я не прав, делайте что хотите.


  1. lair
    14.12.2015 11:08
    +4

    Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose

    Вообще-то у StreamReader есть параметр, который это отключает.


    1. Bonart
      14.12.2015 11:59

      Вы правы, вот только и то и другое поведение может быть недостаточно гибко.
      С одной стороны вариант по умолчанию закрывает то, что можно переиспользовать.
      С другой — вариант без автозакрытия ничего не сообщает о том, что Stream больше не используется и им можно свободно распоряжаться.
      IDisposable<T> позволяет гибко настроить оба варианта, причем на уровне CompositionRoot, не трогая код, использующий StreamReader.


      1. creker
        14.12.2015 12:17
        +5

        Такое ощущение, что вы боретесь не с причиной, а со следствием. А причина — изначально плохая архитектура. Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается. Больше он ни о чем думать не должен. Код снаружи должен сообщать, может ли стрим использоваться где-то в другом месте или он еще занят. Уже изначально такая проблема намекает, что где-то что-то не так и IDisposable тут совсем не при чем.


        1. Bonart
          14.12.2015 12:33

          Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается.

          Вообще-то не покрывает. Закрыть или не закрыть Stream означает «переиспользовать или не переиспользовать» уровнем выше.
          И если все-таки переиспользовать, то ответ на вопрос «с какого момента?» может оказаться очень важным.
          Код снаружи тут не при делах: только тот, кто использует Reader может сообщить о том, что зависимости Reader'а больше не нужны и могут быть переиспользованы где-то еще.
          Аналог — переиспользование соединений посредством пула: чтобы вернуть соединение в пул и сделать его доступным для повторного использования, необходимо знать, когда оно больше не нужно текущему клиенту.


          1. FiresShadow
            14.12.2015 13:26

            Имхо, автор открыл (ну, или поспособствовал открытию) замечательный способ множественного наследования из мира ненормального программирования. Вместо того, чтобы класс реализовывал IPrintable, IExcelable, IDisposable можно написать три универсальных класса Printable, Excelable, Disposable, способных печатать и диспозить что угодно. Всю логику передаём в универсальные классы через делегаты. Поскольку в C#/Java нет множественного наследования, то делаем Printable[Excelable[Disposable[ViewModel]]].
            Можно вообще сделать просто один класс Doer[T], делающий вообще всё что угодно. Пусть у класса 3 метода, тогда переменная будет иметь тип Doer[Doer[Doer[StreamReader]]](). Код вызова 1го метода: doer.Do(). Второго: doer.Value.Do(). Третьего: doer.Value.Value.Do(). Логику методов можно передавать через делегаты: new StreamReader.ToDoer(x => x.Dispose()).ToDoer(x => x.Value.ToString()).ToDoer(x => x.Value.Value.GetHashCode()). Чудесненько.

            Имхо, в данном случае автор предлагает своеобразный способ, как «переопределить» Dispose в StreamReader, не реализуя наследника от StreamReader. Это незначительно экономит время: вместо объявления класса и метода нужно просто передать тело метода через делегат в ToDisposable(Action[T] delegate). Цена: использование нестандартного подхода, замена переменных типа StreamReader на Disposable[StreamReader], возможность переопределить только один метод (иначе приходим к Printable[Excelable[Disposable[StreamReader]]], а это уже явный перебор). Этот способ имеет смысл, когда нужно переопределить метод в запечатанном классе без публичных конструкторов. Однако автор предлагает использовать его повсеместно при любом освобождении ресурсов. Непонятно, почему бы просто не реализовать наследника StreamReader, раз уж так сильно хочется переопределить Dispose. Хотя не до конца понятно, а зачем его переопределять. Взяли из пула соединение, передали в функцию, функция отработала, вернули соединение в пул.


      1. lair
        14.12.2015 13:42

        IDisposable<T> позволяет гибко настроить оба варианта

        Не в данном случае. StreamReader принимает на вход Stream, а не IDisposable.

        Ну и да, отдельно хочу заметить, что — учитывая стандартные сценарии использования StreamReader/Writer, — я совершенно не понимаю, зачем куда-то о чем-то сигнализировать. Этот флаг покрывал все случавшиеся в моей практике варианты работы.


    1. mird
      14.12.2015 13:08

      Скажем честно, этот параметр — плохая архитектура.


      1. lair
        14.12.2015 13:43
        +3

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


        1. mird
          14.12.2015 13:53

          Я бы сделал так, что если мы стрим передали снаружи — его не закрывать. Если создали внутри то закрывать. А иначе, поведение не очевидное.


          1. lair
            14.12.2015 14:12
            +2

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


  1. Nanako
    14.12.2015 15:00
    +1

    Внесу свои 5 копеек. У меня один процесс от другого получает данные в таких объемах, что приходится переводить GC в режим Sustained Low Latency. Внутри цикла, который иногда вызывается аж раз в 60 микросекунд, создаются и уничтожаются достаточно большие managed и unmanaged массивы, т.к. новые данные приходят через MemoryMap (ага, и StreamReader есть, точнее 8). Чтобы это не отъедало оперативную память со скоростью в 500мб в минуту у всего что там временно используется вызывается Dispose, чтобы наверняка. А еще чтобы не тормозило там куча unsafe и unchecked кода, доступ к коллекциям из локальных переменных, сознательное использование структур и их boxing, ни одной лямбды или замыкания, вобщем все грабли C# вплоть до ручного контроля Capacity коллекций. Короче, код где реально без нормального Dispose вообще никак, еще и с кучей unmanaged ресурсов. И ни разу мне не понадобилось ничего, кроме канонической реализации IDisposable и пары классов оберток. А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай. В реальном коде который доводит систему до under memory pressure ваша реализация ИМХО гарантированно даст отрицательную эффективность. Я не говорю что ваниль спасет человечество, но такие велосипеды говорят о кривой архитектуре. Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.


    1. Bonart
      14.12.2015 16:34

      Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.

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

      Не могу понять где вы у меня нашли наследование классов, вызовы виртуальный функций и т.п.


      1. Nanako
        14.12.2015 16:55
        +1

        Ну у вас как бы два варианта развития событий, код используете только вы, и там где код вызывается редко, можно все залить сахаром в виде LINQ, Rx и все такое. А второй вариант это если код использую другие люди, или он является ядром системы. И в итоге есть когда пара солюшенов, каждый на 500 файлов не считая тестов, и в одном что-то тормозит из-за косяков в другом начинаются проблемы, из серии «билд не может закончится т.к. таргет dll занята в мокапе для интегрейшн теста другого солюшена, а перезапускать сервер на каждый билд мы задолбались уже». И очень быстро приучаешься писать с минимумом наследования, абстракт классами и seal'ом всего и вся. И инициализация ресурсов, и финализация, и Dispose начинают прописываться вообще до бизнес логики. А после продакшена вообще оказывается что кастом методы Dispose прописанные для каждого класса это мана небесная, вроде «Так, а здесь для экономии ресурсов и увеличения скорости мы вообще откажемся от GC и лог замаршаллим прямо строками в heap», когда внезапно боттлнеком оказывается синхронное логгирование в одном из тредлупов из-за загруженности тома другим процессом на сервере.

        И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах. А по изначальной логике C# есть набор базовых функций например IDisposable, IComparable и все такое, далее делается несколько абстракт классов с нужными наборами функциональности, и потом в идеальном случае один финальный класс где уже все функции определены и по максимуму финализированы. И этажерка virtual calls минимальная, и дженерики генерируются очень эффективно. А вы пытаетесь какой-то свой стиль привить, и в итоге что там в стеке MSIL получится мне страшно представить.


        1. Bonart
          14.12.2015 17:11

          И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах.

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


          1. Nanako
            14.12.2015 17:20
            +1

            Тут дело в том что вы на функциональность которая идет «снизу» хотя бы в рамках GC и всего такого пытаетесь навесить инвершен и зайти «сверху». И архитектура трещит, и абсолютно реально появление конструкций «Printable<Excelable<Disposable[ViewModel]>>» если лепить концепт IoC на все подряд. Хотя тут старый добрый ООП отлично подошел бы, а чтобы не поощрять программиста устраивать вложенные virtual calls C# еще и по рукам бьет.


            1. Bonart
              15.12.2015 13:52

              Видите ли, то, что я делаю, это и есть старый добрый ООП.
              Что значит «архитектура трещит»?
              Те оптимизации, про которые вы говорите — всего лишь издержки конкретной реализации платформы и не надо выдавать нужду за добродетель.


  1. Viacheslav01
    14.12.2015 16:04
    +1

    Зачем, зачем все это? Если ты сам не знаешь, о используемых объектах, что, как, где, зачем и как долго, то никакие велосипеды тебя не спасут, только добавят сложности в твой код, больше, больше и еще больше!


  1. IL_Agent
    15.12.2015 13:10

    При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами

    Несколько владельцев у одного disposable объекта? Просто не надо так делать.


    1. Bonart
      15.12.2015 13:48
      -1

      Вы в курсе про паттерн Dependency Injection? Объект НЕ владеет своими зависимостями, владелец у них один — Composition Root.


      1. IL_Agent
        15.12.2015 13:56

        Несколько классов зависят от одного disposable класса? Просто не надо так делать.
        Лучше? )


        1. Bonart
          15.12.2015 14:09

          Не лучше.
          Допустим, у вас есть есть очень дорогой объект. Он долго создается и жрет четверть всей доступной вам памяти за счет требующих очистки ресурсов. Реализуемый им интерфейс допускает параллельную работу без ограничений. Также у вас есть 10 дешевых объектов, которые зависят от интерфейса, реализованного дорогим.
          Теперь попробуйте последовать собственному совету, подумайте, что будет и как исправить ситуацию.


          1. IL_Agent
            15.12.2015 20:30

            Когда эти ресурсы требуют освобождения? Обычно такие объекты регистрируются как синглтон и умирают вместе с процессом. Соответственно, такой объект не должен быть idisposable.


            1. Bonart
              15.12.2015 20:38

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


              1. lair
                15.12.2015 20:55

                Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.

                Это почему?


                1. Bonart
                  17.12.2015 11:10

                  > Это почему?
                  Потому что иначе класс будет содержать утечки ресурсов by design


                  1. lair
                    17.12.2015 11:45

                    Потому что иначе класс будет содержать утечки ресурсов by design

                    И в какой момент будут утекать ресурсы, учитывая, что экземпляр класса ровно один на процесс?


                    1. Bonart
                      17.12.2015 11:49

                      А вот это на уровне класса неизвестно. И не должно быть известно.


                      1. lair
                        17.12.2015 11:51

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


                        1. Bonart
                          17.12.2015 12:41

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


                          1. lair
                            17.12.2015 12:47

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

                            Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора», есть «я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы».


                            1. Bonart
                              20.12.2015 01:12

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

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

                              Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.
                              «я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы»

                              Как правило знаю. Но на уровне реализации знать не хочу, так это будет неявной зависимостью.


                              1. lair
                                20.12.2015 18:34
                                +1

                                Недостающий код — оверхед много больший.

                                Как вы определяете, что его не достает?

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

                                O, srsly? Давайте на примерах. Вот, значит, класс «с ресурсами»:

                                public class DisposableResourceHolder : IDisposable {
                                
                                    private SafeHandle resource; // handle to a resource
                                
                                    public DisposableResourceHolder(){
                                        this.resource = ... // allocates the resource
                                    }
                                
                                    public void Dispose(){
                                        Dispose(true);
                                        GC.SuppressFinalize(this);
                                    }
                                
                                    protected virtual void Dispose(bool disposing){
                                        if (disposing){
                                            if (resource!= null) resource.Dispose();
                                        }
                                    }
                                }
                                


                                А вот регистрация:
                                container.Register<DisposableResourceHolder>().AsSingleInstance();
                                


                                Два вопроса:
                                — «течет» ли этот класс?
                                — в какой момент будет гарантированно вызван resource.Dispose?

                                А теперь давайте сделаем вот так:
                                public class DisposableResourceHolder {
                                
                                    private SafeHandle resource; // handle to a resource
                                
                                    public DisposableResourceHolder(){
                                        this.resource = ... // allocates the resource
                                    }
                                }
                                
                                container.Register<DisposableResourceHolder>().AsSingleInstance();
                                


                                Что изменилось в ответах на поставленные выше вопросы?

                                Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.

                                Так почему же объект, имеющий не-GC-ресурсы, обязан иметь IDisposable? Что изменится, если он не будет его иметь?

                                Но на уровне реализации знать не хочу, так это будет неявной зависимостью.

                                А зря. Короткоживущие и долгоживущие объекты могут иметь сильно разные внутреннюю реализацию.


              1. IL_Agent
                15.12.2015 23:54
                +2

                Не стоит пытаться подогнать условия под ответ.

                «Дорогой объект, который жрет четверть всей доступной вам памяти за счет требующих очистки ( в подходящий момент !) ресурсов». Это условие? )) В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.

                Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.


                На самом деле IDisposable — это аналог RAII. Отличие в том, что там деструктор вызывается гарантировано и детерминировано, а в c# для этого используется связка using/dispose. Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using. Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.


                1. Bonart
                  17.12.2015 11:08

                  В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.

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

                  Нет. using всего лишь удобный синтаксический сахар для использование IDisposable в пределах одного метода.
                  Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.

                  Autofac позволяет не заморачиваться с явным освобождением. Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.

                  Так что вы будете делать в условиях задачи?


                  1. IL_Agent
                    17.12.2015 16:50

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

                    То, как класс следует использовать, определяется при проектировании класса. И если класс реализует IDisposable, то это означает, что для его объектов должен быть обязательно вызван Dispose(). Через using, finally или ещё как-нибудь, но вызван. Это не значит, что Dispose нам дали, а мы решаем, дёргать его или нет.

                    Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.

                    Так что вы будете делать в условиях задачи?

                    Приведите условие конкретной задачи, как я привёл выше. «Сделайте класс, который может освобождать ресурсы в произвольный момент» = «Реализуйте IDisposable», да. Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.

                    Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.

                    Кто вызывает Dispose у LifetimeScope?


                    1. Bonart
                      17.12.2015 19:01

                      То, как класс следует использовать, определяется при проектировании класса.

                      И является его контрактом.
                      Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.
                      А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн, нарушающий, для начала, принцип единственной ответственности.
                      Такая практика чревата размножением копипасты, непригодной для автономного тестирования и повторного использования.
                      Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.

                      Класс не должен контролировать количество своих экземпляров. Для этого есть коллекции, пулы, контейнеры и т.п.
                      Если мне надо держать файл открытым — это обеспечит один класс. А единственность его экземпляра в рамках процесса — другой класс.
                      Приведите условие конкретной задачи

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

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

                      Composition Root.


                      1. IL_Agent
                        17.12.2015 23:58

                        Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.

                        Пруф?

                        А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн

                        А где вы у меня это увидели? Впрочем, споры о том, чем является синглтон, паттерном ли, антипаттерном, не утихают по сей день :)

                        В задаче достаточно информации для решения.

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