Wie kann ich Ereignisabonnements in C # löschen?


141

Nehmen Sie an der folgenden C # -Klasse teil:

c1 {
 event EventHandler someEvent;
}

Wenn es viele Abonnements für c1die someEventVeranstaltung gibt und ich sie alle löschen möchte, wie kann ich dies am besten erreichen? Bedenken Sie auch, dass Abonnements für diese Veranstaltung Lambdas / anonyme Delegierte sein können / sind.

Derzeit besteht meine Lösung darin, eine ResetSubscriptions()Methode hinzuzufügen c1, die someEventauf null gesetzt ist. Ich weiß nicht, ob dies unsichtbare Konsequenzen hat.

Antworten:


181

Innerhalb der Klasse können Sie die (versteckte) Variable auf null setzen. Eine Nullreferenz ist die kanonische Methode, um eine leere Aufrufliste effektiv darzustellen.

Von außerhalb der Klasse können Sie dies nicht tun - Ereignisse legen im Grunde "Abonnieren" und "Abbestellen" offen und das war's.

Es lohnt sich zu wissen, was feldähnliche Ereignisse tatsächlich tun - sie erstellen gleichzeitig eine Variable und ein Ereignis. Innerhalb der Klasse verweisen Sie am Ende auf die Variable. Von außen verweisen Sie auf das Ereignis.

Weitere Informationen finden Sie in meinem Artikel zu Veranstaltungen und Delegierten .


3
Wenn Sie stur sind, können Sie es durch Reflexion klar machen. Siehe stackoverflow.com/questions/91778/… .
Brian

1
@Brian: Das hängt von der Implementierung ab. Wenn es sich nur um ein feldähnliches Ereignis handelt EventHandlerList, können Sie dies möglicherweise. Sie müssten diese beiden Fälle jedoch erkennen - und es könnte eine beliebige Anzahl anderer Implementierungen geben.
Jon Skeet

@Joshua: Nein, die Variable wird auf den Wert null gesetzt. Ich bin damit einverstanden, dass die Variable nicht aufgerufen wird hidden.
Jon Skeet

@ JonSkeet Das habe ich (gedacht) gesagt. Die Art und Weise, wie es geschrieben wurde, verwirrte mich 5 Minuten lang.

@JoshuaLamusga: Nun, Sie sagten, es würde eine Aufrufliste löschen, was so klingt, als würde man ein vorhandenes Objekt ändern.
Jon Skeet

34

Fügen Sie c1 eine Methode hinzu, die 'someEvent' auf null setzt.

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

Das ist das Verhalten, das ich sehe. Wie ich in meiner Frage sagte, weiß ich nicht, ob ich etwas übersehen habe.
Programmierer

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

Es ist besser zu verwenden, delegate { }als nulldie Null-Ref-Ausnahme zu vermeiden.


2
Warum? Könnten Sie bitte diese Antwort erweitern?
S. Buda

1
@ S.Buda Denn wenn es null ist, bekommst du eine null ref. Es ist wie mit einem List.Clear()vs myList = null.
AustinWBryan

6

Das Setzen des Ereignisses auf null innerhalb der Klasse funktioniert. Wenn Sie eine Klasse entsorgen, sollten Sie das Ereignis immer auf null setzen. Der GC hat Probleme mit Ereignissen und bereinigt die entsorgte Klasse möglicherweise nicht, wenn baumelnde Ereignisse vorliegen.


6

Die beste Vorgehensweise zum Löschen aller Abonnenten besteht darin, someEvent durch Hinzufügen einer weiteren öffentlichen Methode auf null zu setzen, wenn Sie diese Funktionalität für externe Benutzer verfügbar machen möchten. Dies hat keine unsichtbaren Konsequenzen. Voraussetzung ist, dass Sie SomeEvent mit dem Schlüsselwort 'event' deklarieren.

Bitte lesen Sie das Buch - C # 4.0 auf den Punkt gebracht, Seite 125.

Jemand hier schlug vor, Delegate.RemoveAllMethode zu verwenden . Wenn Sie es verwenden, kann der Beispielcode dem folgenden Formular folgen. Aber es ist wirklich dumm. Warum nicht einfach SomeEvent=nullinnerhalb der ClearSubscribers()Funktion?

public void ClearSubscribers ()
{
   SomeEvent = (EventHandler) Delegate.RemoveAll(SomeEvent, SomeEvent);
   // Then you will find SomeEvent is set to null.
}

5

Sie können dies mithilfe der Methoden Delegate.Remove oder Delegate.RemoveAll erreichen.


6
Ich glaube nicht, dass dies mit Lambda-Ausdrücken oder anonymen Delegierten funktioniert.
Programmierer

3

Konzeptionell erweiterter langweiliger Kommentar.

Ich verwende lieber das Wort "Ereignishandler" anstelle von "Ereignis" oder "Delegieren". Und benutzte das Wort "Ereignis" für andere Sachen. In einigen Programmiersprachen (VB.NET, Object Pascal, Objective-C) wird "Ereignis" als "Nachricht" oder "Signal" bezeichnet und verfügt sogar über ein Schlüsselwort "Nachricht" und eine bestimmte Zuckersyntax.

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;

Um auf diese "Nachricht" zu antworten, antwortet ein "Ereignishandler", unabhängig davon, ob es sich um einen einzelnen oder mehrere Delegierte handelt.

Zusammenfassung: "Ereignis" ist die "Frage", "Ereignishandler" sind die Antwort (en).


1

Das ist meine Lösung:

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

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

Sie müssen Muster aufrufen Dispose()oder verwenden using(new Foo()){/*...*/}, um alle Mitglieder der Aufrufliste abzumelden.


0

Entfernen Sie alle Ereignisse. Nehmen Sie an, dass das Ereignis vom Typ "Aktion" ist:

Delegate[] dary = TermCheckScore.GetInvocationList();

if ( dary != null )
{
    foreach ( Delegate del in dary )
    {
        TermCheckScore -= ( Action ) del;
    }
}

1
Wenn Sie sich innerhalb des Typs befinden, der das Ereignis deklariert hat, müssen Sie dies nicht tun. Sie können es einfach auf null setzen. Wenn Sie sich außerhalb des Typs befinden, können Sie die Aufrufliste des Delegaten nicht abrufen. Außerdem löst Ihr Code beim Aufruf eine Ausnahme aus, wenn das Ereignis null ist GetInvocationList.
Servy

-1

Anstatt Rückrufe manuell hinzuzufügen und zu entfernen und eine Reihe von Delegatentypen überall deklarieren zu lassen:

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

Sie könnten diesen generischen Ansatz ausprobieren:

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

Können Sie bitte Ihre Frage formatieren und den gesamten Leerraum links entfernen? Wenn Sie von einer IDE kopieren und einfügen, kann dies passieren
AustinWBryan

Nur losgeworden diesen weißen Raum, meinen schlechten
barthdamon
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.