Использование IDisposable для отмены подписки на события

У меня есть класс, который обрабатывает события из элемента управления WinForms. В зависимости от того, что делает пользователь, я определяю один экземпляр класса и создаю новый для обработки того же события. Мне нужно сначала отписать старый экземпляр от события - достаточно просто. Я хотел бы сделать это, если возможно, без использования собственных средств, и похоже, что это работа для IDisposable. Однако большая часть документации рекомендует IDisposable только при использовании неуправляемых ресурсов, что здесь не применяется.

Если я реализую IDisposable и откажусь от подписки на событие в Dispose (), искажаю ли я его намерения? Должен ли я вместо этого предоставить функцию Unsubscribe () и вызвать ее?


Изменить: вот какой-то фиктивный код, который показывает, что я делаю (используя IDisposable). Моя реальная реализация связана с некоторой проприетарной привязкой данных (длинная история).

class EventListener : IDisposable
{
    private TextBox m_textBox;

    public EventListener(TextBox textBox)
    {
        m_textBox = textBox;
        textBox.TextChanged += new EventHandler(textBox_TextChanged);
    }

    void textBox_TextChanged(object sender, EventArgs e)
    {
        // do something
    }

    public void Dispose()
    {
        m_textBox.TextChanged -= new EventHandler(textBox_TextChanged);
    }
}

class MyClass
{
    EventListener m_eventListener = null;
    TextBox m_textBox = new TextBox();

    void SetEventListener()
    {
        if (m_eventListener != null) m_eventListener.Dispose();
        m_eventListener = new EventListener(m_textBox);
    }
}

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


Заключение

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

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


person Jon B    schedule 16.01.2009    source источник
comment
см. шаблон WeakEvent, который может вам помочь: msdn.microsoft.com/en-us /library/aa970850.aspx   -  person gbjbaanb    schedule 17.01.2009
comment
Прошло 7 лет, и эта ссылка гласит: К сожалению, запрошенная вами тема больше не доступна. Используйте поле поиска, чтобы найти соответствующую информацию.   -  person Rhyous    schedule 29.04.2016


Ответы (9)


Да, дерзай. Хотя некоторые люди думают, что IDisposable реализован только для неуправляемых ресурсов, это не так - неуправляемые ресурсы оказываются самым большим преимуществом и наиболее очевидной причиной для его реализации. Я думаю, он приобрел эту идею, потому что люди не могли придумать никакой другой причины для ее использования. Это не похоже на финализатор, который является проблемой производительности, и GC нелегко справиться с этим.

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

Цель IDisposable - улучшить работу вашего кода без необходимости выполнять много ручной работы. Используйте его силу в свою пользу и избавьтесь от какой-то искусственной ерунды о «замысле дизайна».

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

person gbjbaanb    schedule 16.01.2009
comment
Я категорически не согласен. Когда вы это делаете, вы нарушаете контракт в дизайне за контрактом. - person Domenic; 17.01.2009
comment
какой контракт? Нигде не говорится, что IDisposable предназначен только для неуправляемых ресурсов. Часто говорится, что можно использовать для чего, но это большая разница. - person gbjbaanb; 17.01.2009
comment
@Domenic: Я согласен с gbjbaanb. Даже если в документации действительно сказано, что IDisposable предназначен только для выпуска неуправляемых ресурсов, вы не нарушите какой-либо жесткий контракт (как в предварительных условиях, постусловиях и инвариантах классов), когда будете использовать его для другой очистки. - person ; 17.01.2009
comment
Разве отпускание слушателей не считается уборкой? - person Benjamin Autin; 05.08.2009
comment
Я размышлял над тем же вопросом, что и OP, и где-то вроде решил разрешить использование, отличное от неуправляемой очистки ресурсов, но из того, что я могу сказать, большая часть документации строго предписывает разработчикам использовать шаблон IDisposable для удаления неуправляемых или управляемых ресурсов. Я не могу найти ссылку, но где-то управляемые ресурсы были описаны как управляемые объекты, которые владеют управляемыми или неуправляемыми ресурсами, которые также реализуют IDisposable. Я еще не уверен, считается ли отклонение от этих рекомендаций опасным или плохим. - person jpierson; 14.04.2011
comment
Рассмотрите возможность использования SafeHandle (blogs.msdn.com/ b / bclteam / archive / 2005/03/16 / 396900.aspx) для примера того, как MS не продумала все достаточно хорошо :) IMHO единственное, что вы можете оставить GC, это объекты, которые используйте только память. Если он содержит какие-либо другие ресурсы, управляемые или неуправляемые, тогда можно использовать IDisposable для очистки объекта, когда вы захотите. - person gbjbaanb; 19.04.2011
comment
Проблема, которую я обнаружил при таком подходе, заключается в том, что почти все ваши классы реализуют IDisposable. Вы добавляете обработчик событий в класс, таким образом, вы реализуете IDisposable в этом классе. Затем вы должны реализовать IDisposable для всех классов, используя предыдущий класс для вызова Dispose при завершении работы с этим классом. Вскоре вы обнаружите, что используете IDisposable в половине ваших классов, что не соответствует предполагаемому использованию интерфейса. - person Ignacio Soler Garcia; 10.06.2012
comment
Не уверен, что это может быть решением, но как насчет шаблона «Слабое событие»? - person Ignacio Soler Garcia; 10.06.2012
comment
@SoMoS: единственное, что должно вызывать Dispose для объекта, - это последняя сущность, которая несет за это ответственность. Если объект, которому дана ссылка на объект, будет ожидать использовать его дольше, чем объект, который в настоящее время за него отвечает, эти два объекта должны согласиться, чтобы получатель взял на себя ответственность. В противном случае передача или ответственность (и Dispose) не требуются. Кроме того, если объект подписывается на уведомления о событиях, но не реализует IDisposable, как издатель должен знать, что уведомления больше не требуются? - person supercat; 25.08.2013
comment
Я думаю, что еще одна причина для реализации IDisposable - это возможность использовать ваш объект внутри оператора using: using (var vm = new DisposableViewModel ()) {...} - person 3615; 10.02.2017

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

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

person JaredPar    schedule 16.01.2009
comment
В WPF почти нет элементов управления IDisposable, поскольку он использует шаблон WeakEvent для обхода утечек: msdn .microsoft.com / en-us / library / aa970850.aspx. - person gbjbaanb; 17.01.2009
comment
@gbjbaanb - Правильно, это то, что я тоже понял, хотя это, вероятно, подтверждает точку зрения JaredPar об указании на то, что событиями следует управлять другим способом. Я полагаю, что одним из этих способов может быть шаблон WeakEvent, другим - пользовательский интерфейс IUnsubscribable, например, который может имитировать использование IDisposable. - person jpierson; 14.04.2011
comment
У @jpierson, имеющего интерфейс IUnsubscribable, есть недостаток: это сделало бы невозможным написать что-то вроде using (var vm = new UnsibscribableViewModel ()) {...}. - person 3615; 10.02.2017
comment
WeakEventPattern - это способ решить проблему с OP. Именно для этой цели он был разработан в версии 3.0 RT. - person bokibeg; 01.06.2017
comment
Это слишком догматично. Почему бы не использовать IDisposable? Вы можете реализовать свой метод Unsubscribe, но он не будет вызываться в конце оператора Using или контейнером IOC, который внедрил объект, тогда как Dispose будет. Если вам нужно очистить, используйте IDisposable. - person Mick; 22.02.2019

Одна вещь, которая беспокоит меня при использовании шаблона IDisposable для отказа от подписки на события, - это проблема финализации.

Dispose() функция в IDisposable должна вызываться разработчиком, ОДНАКО, если она не вызывается разработчиком, подразумевается, что GC вызовет эту функцию (по крайней мере, по стандартному шаблону IDisposable). Однако в вашем случае, если вы не вызываете Dispose, никто другой не будет - событие остается, и сильная ссылка удерживает GC от вызова финализатора.

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

person VitalyB    schedule 26.10.2010
comment
@ Джейсон Койн: Я бы присоединился к Джейсону Койну в его доводе до противоположного: iDisposable предназначен для вещей, с которыми нельзя удовлетворительно справиться только с помощью сборки мусора. В самом деле, моя интерпретация контракта iDisposable будет следующей: / only / объекты, которые / не должны / реализовывать iDisposable, - это те, для которых достаточно очистки на основе GC, или, возможно, те, которые не могут быть очищены с помощью одного вызова метода . На мой взгляд, / не реализация / iDisposable делает более сильное заявление, чем его реализация. - person supercat; 01.11.2010
comment
Через некоторое время с моим кодом я склонен соглашаться с вами. Однако меня по-прежнему беспокоит двусмысленность IDisposable, и я хочу, чтобы Microsoft лучше справилась с этой проблемой. - person VitalyB; 02.11.2010
comment
Я полностью согласен с тобой. IDisposable был реализован с самого начала для обработки COM-взаимодействия и интеграции с неуправляемыми ресурсами. Предоставление решения, которое гарантирует отсутствие утечек памяти, - это хорошо, но, как вы отметили, если вы используете Dispose () для отказа от подписки на события и не вызываете метод непосредственно в коде (т.е. оператор using или иначе), тогда сильная ссылка удерживается, и объект никогда не попадает в сборку мусора. Это боль в спине, и определенно есть о чем поговорить. - person Doug; 20.11.2010
comment
@Doug: Тот факт, что неспособность очистить обработчики событий приведет к утечке памяти, раздражает, но гораздо разумнее сказать, когда это возможно, должно быть возможно очистить любой объект, попробовав приведение к IDisposable и, если это удастся , вызывая Dispose на нем, чем сказать, что объекты, которые не могут очистить себя автоматически, когда метод очистки не вызывается, не должны реализовывать IDisposable, тем более что использование IDisposable,Dispose в качестве метода очистки значительно увеличивает вероятность того, что он будет вызван. - person supercat; 03.08.2012
comment
@VitalyB, вы можете исправить это, используя, конечно, слабую ссылку вместо сильной. В этом случае GC может вызвать финализатор. - person Tim Lovell-Smith; 21.08.2013
comment
@supercat Использование IDisposable для удаления финализируемых ресурсов вашего объекта сейчас вместо того, чтобы позволить GC сделать это позже, является допустимым использованием IDisposable и фактически оптимизацией, поскольку освобождает место в очереди финализатора, когда вы вызываете SuppressFinalize (), и конечно, это высвобождает все ресурсы, которые высвобождает ваш финализатор. Вот почему «стандартный» пример для IDisposable делает это. - person Tim Lovell-Smith; 21.08.2013
comment
@ TimLovell-Smith: Для объектов, которые могут выполнять нормальную работу по автоматической очистке, нормально предлагать IDisposable, чтобы код мог работать еще лучше. Я прочитал исходный ответ (особенно последний абзац), однако, как утверждающий, что вещи, которые не могут очищаться автоматически, не должны реализовывать IDisposable, потому что вызывающие абоненты могут рассматривать тот факт, что класс реализует IDisposable как признак того, что класс будет может автоматически убирать за собой, не вызывая у потребителя вызова такого метода очистки, как Dispose. - person supercat; 21.08.2013

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

public class DisposableEvent<T> : IDisposable
    {

        EventHandler<EventArgs<T>> Target { get; set; }
        public T Args { get; set; }
        bool fired = false;

        public DisposableEvent(EventHandler<EventArgs<T>> target)
        {
            Target = target;
            Target += new EventHandler<EventArgs<T>>(subscriber);
        }

        public bool Wait(int howLongSeconds)
        {
            DateTime start = DateTime.Now;
            while (!fired && (DateTime.Now - start).TotalSeconds < howLongSeconds)
            {
                Thread.Sleep(100);
            }
            return fired;
        }

        void subscriber(object sender, EventArgs<T> e)
        {
            Args = e.Value;
            fired = true;
        }

        public void Dispose()
        {
            Target -= subscriber;
            Target = null;
        }

    }

который позволяет вам написать этот код:

Class1 class1 = new Class1();
            using (var x = new DisposableEvent<object>(class1.Test))
            {
                if (x.Wait(30))
                {
                    var result = x.Args;
                }
            }

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

person Jason Coyne    schedule 05.08.2009
comment
Одноразовый шаблон на самом деле не потому, что GC не может делать это автоматически, одноразовый шаблон заключается в том, что имеет смысл поддерживать сценарий «очистить как можно скорее» в блоке using. Финализатор необходим для очистки ресурсов, сборщик мусора не знает, как очистить, и поэтому не может делать это автоматически. - person Tim Lovell-Smith; 21.08.2013

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

P.S. [OT] Я считаю решение предоставить только сильных делегатов единственной самой дорогостоящей ошибкой проектирования платформы .NET.

person Community    schedule 17.01.2009
comment
Не уверен, что самое дорогое, но оно есть. Я озадачен, почему Microsoft до сих пор не предложила тип WeakDelegate, а Delegate.Combine и Delegate.Remove исключили из итогового списка делегатов все слабые делегаты, срок действия которых истек. Слабые события на стороне издателя не являются подходящим решением, поскольку именно подписчик, а не издатель, знает, должна ли подписка поддерживать объект в действии. - person supercat; 28.09.2012

Из всего, что я читал об одноразовых расходных материалах, я бы сказал, что они были в основном изобретены для решения одной проблемы: своевременного освобождения неуправляемых системных ресурсов. Но все же все примеры, которые я нашел, не только сосредоточены на теме неуправляемых ресурсов, но также имеют еще одно общее свойство: Dispose вызывается только для ускорения процесса, который в противном случае произошел бы позже автоматически (GC -> finalizer -> dispose)

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

Итак, основное отличие состоит в том, что события каким-то образом создают граф объектов, который невозможно собрать, поскольку объект обработки событий внезапно становится ссылкой службы, на которую вы просто хотели ссылаться / использовать. Вы внезапно вынуждены вызвать Dispose - автоматическое удаление невозможно. Таким образом, Dispose получит другое тонкое значение, отличное от того, которое можно найти во всех примерах, где вызов Dispose - в грязной теории;) - не является необходимым, поскольку он будет вызываться автоматически (в какой-то момент) ...

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

Чего мы хотим достичь, так это отключить / остановить какое-то поведение (отказавшись от подписки на событие). Было бы неплохо иметь стандартный интерфейс, такой как IStoppable, с методом Stop (), который по контракту просто ориентирован на

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

Назовем единственный интерфейсный метод, который выполняет отписку «Stop ()». Вы бы знали, что остановленный объект находится в приемлемом состоянии, но только остановлен. Может быть, было бы неплохо иметь простое свойство «Остановлен».

Было бы даже разумно иметь интерфейс «IRestartable», который наследуется от IStoppable и дополнительно имеет метод «Restart ()», если вы просто хотите приостановить определенное поведение, которое, безусловно, понадобится снова в будущем, или сохранить удаленный объект модели в истории для последующего отмены восстановления.

После всего написания я должен признаться, что только что видел пример IDisposable где-то здесь: http://msdn.microsoft.com/en-us/library/dd783449%28v=VS.100%29.aspx Но в любом случае, пока я не получу все детали и исходную мотивацию из IObservable я бы сказал, что это не лучший пример использования

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

Но, похоже, они на правильном пути. В любом случае: они должны были использовать мой интерфейс "IStoppable";), так как я твердо уверен, что есть разница в

  • Dispose: «вы должны вызвать этот метод, иначе что-то может протечь , если сборщик мусора опаздывает» ....

а также

  • Стоп: «вы должны вызвать этот метод, чтобы остановить определенное поведение»
person Sebastian Gregor    schedule 05.01.2011
comment
Я не согласен. Существует строгое соглашение, согласно которому, если объект требует очистки, его владелец может очистить его, попытавшись привести его к IDisposable и, если это сработает, вызвать Dispose. Не нужно гадать, может ли объект потребовать какой-либо другой очистки. Хотя верно, что многим одноразовым объектам удастся очистить себя, если они были оставлены, вывод о том, что объект, реализующий IDisposable, очистит себя, когда заброшен, намного слабее, чем вывод о том, что объект, который не реализация IDisposable сделает это. - person supercat; 05.05.2011
comment
Между прочим, я думаю, что шаблон IDisposable по умолчанию для MS - это глупо (единственными объектами с финализаторами очистки должны быть те, чьей целью является инкапсуляция одной ответственности за очистку; если класс делает что-то иное, кроме обработки одной ответственности за очистку, то по определению любые производные классы также будет, и, таким образом, производные классы не должны иметь финализаторов для очистки (они могут иметь финализаторы, генерирующие ошибочно брошенные записи журнала Object). Я также хотел бы отметить, что имя Dispose является неправильным, поскольку цель не в том, чтобы избавиться от объекта , скорее... - person supercat; 05.05.2011
comment
... чтобы позволить ему выполнять любые возложенные на него обязанности (обычно очистку других объектов) до того, как он будет оставлен. - person supercat; 05.05.2011

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

Я тоже голосую за метод отказа от подписки на вашем собственном интерфейсе.

person annakata    schedule 16.01.2009

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

Это может быть, а может и не быть хорошей идеей в вашем конкретном случае - я не думаю, что у нас действительно достаточно информации - но это стоит рассмотреть.

person Jon Skeet    schedule 16.01.2009
comment
В моем случае, я думаю, это означает, что объект будет жить вечно, и, к сожалению, у меня будет утечка памяти. - person Jon B; 17.01.2009
comment
@Jon B - я думаю, что это происходит с большинством событий, не связанных с пользовательским интерфейсом. +1 - person Doug; 20.11.2010
comment
На всякий случай, если объяснение Джона не было ясным, я думаю, что он предлагает, чтобы в некоторых случаях, подобных этому, объекты можно было переработать, а не выбросить для нового экземпляра. Это позволяет использовать существующие подписки на события без необходимости отсоединения. Думайте об этом как о пуле потоков или пуле соединений для ваших объектов, где есть пул возможных объектов, которые можно переработать. Это не решение для всех ситуаций, но, вероятно, в большем количестве случаев, чем нет, если вы просто измените свое мнение об этом. - person jpierson; 14.04.2011

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

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

person Tim Lovell-Smith    schedule 21.08.2013