Як я можу очистити підписки на події в C #?


141

Візьміть такий клас C #:

c1 {
 event EventHandler someEvent;
}

Якщо є багато підписок на c1«S someEventподії , і я хочу , щоб очистити їх все, що є кращим способом для досягнення цієї мети? Також врахуйте, що підписки на цю подію можуть бути / є лямбдами / анонімними делегатами.

В даний час моє рішення полягає в тому, щоб додати ResetSubscriptions()метод до c1цього набору someEventз нуля. Я не знаю, чи це має якісь невидимі наслідки.

Відповіді:


181

Зсередини класу ви можете встановити (приховану) змінну на null. Нульова посилання - це канонічний спосіб ефективного подання порожнього списку викликів.

Поза межами класу ви не можете цього зробити - події в основному розкривають "передплатити" та "скасувати підписку", і все.

Варто бути обізнаним про те, що насправді роблять подібні події - вони створюють змінну та подію одночасно. У межах класу ви посилаєтесь на змінну. Ззовні ви посилаєтесь на подію.

Дивіться мою статтю про події та делегатів для отримання додаткової інформації.


3
Якщо ви впертий, ви можете змусити це зрозуміти за допомогою роздумів. Дивіться stackoverflow.com/questions/91778/… .
Брайан

1
@Brian: Це залежить від реалізації. Якщо це просто польова подія або подію EventHandlerList, ви можете це зробити. Вам доведеться розпізнати ці два випадки - і може бути будь-яка кількість інших реалізацій.
Джон Скіт

@Joshua: Ні, вона встановить змінній значення null. Я згоден, що змінна не буде викликана hidden.
Джон Скіт

@JonSkeet Це я (подумав), що я сказав. Те, як було написано, плутало мене 5 хвилин.

@JoshuaLamusga: Ну, ви сказали, що буде очищено список викликів, який звучить як зміна існуючого об'єкта.
Джон Скіт

34

Додайте метод до c1, який встановить "someEvent" на нуль.

public class c1
{
    event EventHandler someEvent;
    public ResetSubscriptions() => someEvent = null;    
}

Таку поведінку я бачу. Як я вже говорив у своєму запитанні, я не знаю, чи я щось переглядаю.
програміст

8
class c1
{
    event EventHandler someEvent;
    ResetSubscriptions() => someEvent = delegate { };
}

Краще використовувати, delegate { }ніж nullуникати нульового виключення.


2
Чому? Чи можете ви, будь ласка, розширити цю відповідь?
С. Буда

1
@ S.Buda Тому, що якщо це недійсне значення, то ви отримаєте нульову відмову. Це як з використанням List.Clear()проти myList = null.
AustinWBryan

6

Встановлення події на нуль всередині класу працює. Коли ви розпоряджаєтесь класом, ви завжди повинні встановлювати нуль, GC має проблеми з подіями і не може очищати клас disposed, якщо він має звисаючі події.


6

Найкраща практика для очищення всіх передплатників - це встановити значення someEvent на нуль, додавши інший публічний метод, якщо ви хочете викрити цю функціональність зовні. Це не має небачених наслідків. Обов’язковою умовою є пам’ятати, що оголосити SomeEvent ключовим словом «подія».

Будь ласка, дивіться книгу - 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.
}

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;

І щоб відповісти на це "повідомлення", "обробник подій" відповість, будь то один делегат чи кілька делегатів.

Короткий зміст: "Подія" - це "питання", "обробник (и) подій" - це відповіді.


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
Якщо ви знаходитесь у типі, який оголосив подію, вам цього не потрібно робити, ви можете просто встановити його на нуль, якщо ви знаходитесь поза типом, ви не можете отримати список викликів делегата. Також ваш код видає виняток, якщо подія є нульовою під час дзвінка GetInvocationList.
Сервіс

-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();
    }
}

Чи можете ви відформатувати своє запитання та видалити пробіл зліва? Коли ви копіюєте та вставляєте з IDE, це може статися
AustinWBryan

Щойно позбувся цього білого простору, мій поганий
barthdamon
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.