如何在C#中清除事件订阅?

153

考虑以下C#类:

c1 {
 event EventHandler someEvent;
}
如果对于的事件有很多个订阅,我想要清除它们所有,最好的方法是什么? 请考虑到订阅此事件可能是匿名委托/lambda表达式。 目前我的解决方案是添加一个方法到,将设置为null。 我不知道这是否会有任何未预料到的后果。

我在这里使用反射描述了一个可行的答案: https://dev59.com/LHVD5IYBdhLWcg3wGHiu#66956934 - Vassili
10个回答

194

在类中,您可以将(隐藏的)变量设置为 null。空引用是表示空调用列表的规范方式,实际上。

从类外面,您不能这样做 - 事件基本上公开了“订阅”和“取消订阅”,仅此而已。

值得注意的是,字段事件实际上正在做什么 - 它们同时创建一个变量和一个事件。在类内部,您最终会引用该变量。从外部,您引用事件。

有关更多信息,请参见我的有关事件和委托的文章


3
如果你很固执,你可以通过反射来清除它。请参阅 https://dev59.com/LHVD5IYBdhLWcg3wGHiu#91853 。 - Brian
1
@Brian:这取决于具体的实现方式。如果它只是一个类似字段的事件或者EventHandlerList,那么你可能可以这样做。不过你需要识别出这两种情况,而且还有可能存在其他任意数量的实现方式。 - Jon Skeet
@Joshua:不,它会将变量设置为 null 值。我同意这个变量不会被称为“hidden”。 - Jon Skeet
@JonSkeet 这就是我(认为)我说的话。它的书写方式让我困惑了5分钟。 - user1881400
@JoshuaLamusga:你说它会清除调用列表,这听起来像是修改现有对象。 - Jon Skeet

38

为c1添加一个方法,将'someEvent'设置为null。

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

1
这就是我所看到的行为。正如我在问题中所说,我不知道是否忽略了什么。 - programmer

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

使用delegate { }而不是null可以避免空引用异常。


2
为什么?你能否详细解释一下这个答案? - S. Buda
1
@S.Buda 因为如果它是 null,那么你将得到一个空引用。这就像使用 List.Clear()myList = null 的区别。 - AustinWBryan
2
鉴于现代C#中可以执行someEvent?.Invoke(...),我不确定这是否很重要,或者使用noop委托会稍微缺乏清晰度是否值得。 - StayOnTarget

7
清除所有订阅者的最佳实践是通过添加另一个公共方法将 someEvent 设置为 null,如果您想要将此功能暴露给外部。这没有任何不可预见的后果。前提条件是记得用关键字“event”声明 SomeEvent。
请参阅书籍 - C# 4.0 in the nutshell,第125页。
这里有人建议使用 Delegate.RemoveAll 方法。如果您使用它,示例代码可以遵循以下形式。但这真的很愚蠢。为什么不在 ClearSubscribers() 函数内部使用 SomeEvent=null?
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

6
在类内将事件设置为 null 是有效的。 当你销毁一个类时,你应该总是将事件设置为 null,因为 GC 在处理事件时可能会出现问题,如果它有悬空事件,就无法清理已释放的类。

5
您可以使用Delegate.Remove或Delegate.RemoveAll方法来实现此目的。

7
我不相信这会与lambda表达式或匿名委托一起工作。 - programmer

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;

为了响应那个“消息”,需要一个“事件处理程序”来响应,无论是单个代理还是多个代理。

总结: “事件”是“问题”,“事件处理程序(们)”是答案。


2

移除所有事件,假设事件是“动作”类型:

Delegate[] dary = TermCheckScore.GetInvocationList();

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

1
如果您在声明事件的类型内部,则不需要执行此操作,只需将其设置为null即可;如果您在类型之外,则无法获取委托的调用列表。此外,当调用GetInvocationList时,如果事件为null,则您的代码会抛出异常。 - Servy

0

这是我的解决方案:

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()){/*...*/} 模式来取消订阅委托列表中的所有成员。

-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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接