Как очистить подписки на события в C #?

142

Возьмем следующий класс C #:

c1 {
 event EventHandler someEvent;
}

Если есть много подписок на c1«S someEventсобытия , и я хочу , чтобы очистить их все, что является лучшим способом для достижения этой цели? Также учтите, что подписки на это событие могут быть лямбда-выражениями / анонимными делегатами.

В настоящее время мое решение - добавить ResetSubscriptions()метод, для c1которого установлено someEventзначение null. Не знаю, есть ли у этого какие-то невидимые последствия.

программист
источник

Ответы:

182

Изнутри класса вы можете установить для (скрытой) переменной значение null. Пустая ссылка - это канонический способ эффективного представления пустого списка вызовов.

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

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

См. Мою статью о мероприятиях и делегатах для получения дополнительной информации.

Джон Скит
источник
3
Если вы упрямы, вы можете очистить его с помощью отражения. См. Stackoverflow.com/questions/91778/… .
Брайан,
1
@ Брайан: Это зависит от реализации. Если это просто полевое событие или событие EventHandlerList, вы можете это сделать. Однако вам придется распознать эти два случая - и может быть любое количество других реализаций.
Джон Скит,
@ Джошуа: Нет, он установит значение переменной null. Я согласен, что переменная не будет вызываться hidden.
Джон Скит,
@JonSkeet Это то, что я (подумал) я сказал. То, как это было написано, смутило меня на 5 минут.
@JoshuaLamusga: Ну, вы сказали, что это очистит список вызовов, что похоже на изменение существующего объекта.
Джон Скит,
34

Добавьте в c1 метод, который установит для someEvent значение null.

public class c1
{
    event EventHandler someEvent;
    public ResetSubscriptions() => someEvent = null;    
}
программист
источник
Я наблюдаю такое поведение. Как я сказал в своем вопросе, я не знаю, упускаю ли я что-то из виду.
программист
8
class c1
{
    event EventHandler someEvent;
    ResetSubscriptions() => someEvent = delegate { };
}

Лучше использовать, delegate { }чем nullизбегать исключения null ref.

Фэн
источник
2
Зачем? Не могли бы вы расширить этот ответ?
С. Буда
1
@ S.Buda Потому что, если оно равно нулю, вы получите нулевое исх. Это как с использованием List.Clear()против myList = null.
AustinWBryan
6

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

Джонатан С. Дикинсон
источник
6

Лучшая практика для очистки всех подписчиков - установить для someEvent значение null, добавив еще один общедоступный метод, если вы хотите предоставить эту функцию извне. Это не имеет невидимых последствий. Предварительное условие - не забудьте объявить SomeEvent с ключевым словом 'event'.

См. Книгу - Краткое описание C # 4.0, стр. 125.

Кто-то здесь предложил использовать Delegate.RemoveAllметод. Если вы его используете, образец кода может соответствовать приведенной ниже форме. Но это действительно глупо. Почему не только SomeEvent=nullвнутри ClearSubscribers()функции?

public void ClearSubscribers ()
{
   SomeEvent = (EventHandler) Delegate.RemoveAll(SomeEvent, SomeEvent);
   // Then you will find SomeEvent is set to null.
}
Кэри
источник
Delegate.RemoveAll действительны для MulticastDelegate: public delegate string TableNameMapperDelegate(Type type);public static TableNameMapperDelegate TableNameMapper;?
Kiquenet
5

Этого можно добиться с помощью методов Delegate.Remove или Delegate.RemoveAll.

Мика
источник
6
Я не верю, что это сработает с лямбда-выражениями или анонимными делегатами.
программист
3

Концептуальный расширенный скучный комментарий.

Я предпочитаю использовать слово «обработчик событий» вместо «событие» или «делегат». И использовал слово «событие» для других вещей. В некоторых языках программирования (VB.NET, Object Pascal, Objective-C) «событие» называется «сообщением» или «сигналом» и даже имеет ключевое слово «сообщение» и особый синтаксис сахара.

const
  WM_Paint = 998;  // <-- "question" can be done by several talkers
  WM_Clear = 546;

type
  MyWindowClass = class(Window)
    procedure NotEventHandlerMethod_1;
    procedure NotEventHandlerMethod_17;

    procedure DoPaintEventHandler; message WM_Paint; // <-- "answer" by this listener
    procedure DoClearEventHandler; message WM_Clear;
  end;

И, чтобы ответить на это «сообщение», отвечает «обработчик событий», будь то один делегат или несколько делегатов.

Резюме: «Событие» - это «вопрос», «обработчик (и) события» - это ответ (ы).

umlcat
источник
1

Это мое решение:

public class Foo : IDisposable
{
    private event EventHandler _statusChanged;
    public event EventHandler StatusChanged
    {
        add
        {
            _statusChanged += value;
        }
        remove
        {
            _statusChanged -= value;
        }
    }

    public void Dispose()
    {
        _statusChanged = null;
    }
}

Вам нужно вызвать Dispose()или использовать using(new Foo()){/*...*/}шаблон, чтобы отписаться от всех членов списка вызовов.

Джалал
источник
0

Удалите все события, предположим, что это событие относится к типу «Действие»:

Delegate[] dary = TermCheckScore.GetInvocationList();

if ( dary != null )
{
    foreach ( Delegate del in dary )
    {
        TermCheckScore -= ( Action ) del;
    }
}
Гугол
источник
1
Если вы находитесь внутри типа, который объявил событие, которое вам не нужно делать, вы можете просто установить для него значение null, если вы находитесь за пределами типа, вы не можете получить список вызовов делегата. Кроме того, ваш код выдает исключение, если событие имеет значение null при вызове GetInvocationList.
Servy
-1

Вместо того, чтобы добавлять и удалять обратные вызовы вручную и везде объявлять кучу типов делегатов:

// The hard way
public delegate void ObjectCallback(ObjectType broadcaster);

public class Object
{
    public event ObjectCallback m_ObjectCallback;
    
    void SetupListener()
    {
        ObjectCallback callback = null;
        callback = (ObjectType broadcaster) =>
        {
            // one time logic here
            broadcaster.m_ObjectCallback -= callback;
        };
        m_ObjectCallback += callback;

    }
    
    void BroadcastEvent()
    {
        m_ObjectCallback?.Invoke(this);
    }
}

Вы можете попробовать этот общий подход:

public class Object
{
    public Broadcast<Object> m_EventToBroadcast = new Broadcast<Object>();

    void SetupListener()
    {
        m_EventToBroadcast.SubscribeOnce((ObjectType broadcaster) => {
            // one time logic here
        });
    }

    ~Object()
    {
        m_EventToBroadcast.Dispose();
        m_EventToBroadcast = null;
    }

    void BroadcastEvent()
    {
        m_EventToBroadcast.Broadcast(this);
    }
}


public delegate void ObjectDelegate<T>(T broadcaster);
public class Broadcast<T> : IDisposable
{
    private event ObjectDelegate<T> m_Event;
    private List<ObjectDelegate<T>> m_SingleSubscribers = new List<ObjectDelegate<T>>();

    ~Broadcast()
    {
        Dispose();
    }

    public void Dispose()
    {
        Clear();
        System.GC.SuppressFinalize(this);
    }

    public void Clear()
    {
        m_SingleSubscribers.Clear();
        m_Event = delegate { };
    }

    // add a one shot to this delegate that is removed after first broadcast
    public void SubscribeOnce(ObjectDelegate<T> del)
    {
        m_Event += del;
        m_SingleSubscribers.Add(del);
    }

    // add a recurring delegate that gets called each time
    public void Subscribe(ObjectDelegate<T> del)
    {
        m_Event += del;
    }

    public void Unsubscribe(ObjectDelegate<T> del)
    {
        m_Event -= del;
    }

    public void Broadcast(T broadcaster)
    {
        m_Event?.Invoke(broadcaster);
        for (int i = 0; i < m_SingleSubscribers.Count; ++i)
        {
            Unsubscribe(m_SingleSubscribers[i]);
        }
        m_SingleSubscribers.Clear();
    }
}
Barthdamon
источник
Не могли бы вы отформатировать свой вопрос и удалить все пробелы слева? Это может произойти, когда вы копируете и вставляете из IDE
AustinWBryan,
Только что избавился от этого белого пространства, мой плохой
Бартдамон