239

是否可以从事件中取消订阅匿名方法?

如果我订阅这样的事件:

void MyMethod()
{
    Console.WriteLine("I did it!");
}

MyEvent += MyMethod;

我可以像这样取消订阅:

MyEvent -= MyMethod;

但是,如果我使用匿名方法订阅:

MyEvent += delegate(){Console.WriteLine("I did it!");};

是否可以取消订阅此匿名方法?如果是这样,怎么做?

4

13 回答 13

239
Action myDelegate = delegate(){Console.WriteLine("I did it!");};

MyEvent += myDelegate;


// .... later

MyEvent -= myDelegate;

只需保留对代表的引用即可。

于 2008-10-08T15:33:38.177 回答
149

一种技术是声明一个变量来保存匿名方法,然后可以在匿名方法本身内部使用该变量。这对我有用,因为所需的行为是在事件处理后取消订阅。

例子:

MyEventHandler foo = null;
foo = delegate(object s, MyEventArgs ev)
    {
        Console.WriteLine("I did it!");
        MyEvent -= foo;
    };
MyEvent += foo;
于 2008-10-08T15:34:51.497 回答
24

从记忆中看,规范明确地不保证使用匿名方法创建的委托的等效性。

如果您需要取消订阅,您应该使用“普通”方法或将委托保留在其他地方,以便您可以使用与您订阅时完全相同的委托来取消订阅。

于 2008-10-08T15:26:56.067 回答
23

由于C# 7.0 本地函数特性已经发布,J c建议的方法变得非常简洁。

void foo(object s, MyEventArgs ev)
{
    Console.WriteLine("I did it!");
    MyEvent -= foo;
};
MyEvent += foo;

所以,老实说,这里没有匿名函数作为变量。但我认为在您的案例中使用它的动机可以应用于本地功能。

于 2017-08-15T05:24:11.720 回答
17

在 3.0 中可以缩短为:

MyHandler myDelegate = ()=>Console.WriteLine("I did it!");
MyEvent += myDelegate;
...
MyEvent -= myDelegate;
于 2009-07-23T01:53:51.303 回答
11

您可以检测您的类,而不是保留对任何委托的引用,以便将事件的调用列表返回给调用者。基本上你可以这样写(假设 MyEvent 是在 MyClass 中声明的):

public class MyClass 
{
  public event EventHandler MyEvent;

  public IEnumerable<EventHandler> GetMyEventHandlers()  
  {  
      return from d in MyEvent.GetInvocationList()  
             select (EventHandler)d;  
  }  
}

因此,您可以从 MyClass 外部访问整个调用列表并取消订阅您想要的任何处理程序。例如:

myClass.MyEvent -= myClass.GetMyEventHandlers().Last();

我在这里写了一篇关于这种技术的完整文章。

于 2011-06-23T23:11:50.210 回答
6

一种蹩脚的方法:

public class SomeClass
{
  private readonly IList<Action> _eventList = new List<Action>();

  ...

  public event Action OnDoSomething
  {
    add {
      _eventList.Add(value);
    }
    remove {
      _eventList.Remove(value);
    }
  }
}
  1. 覆盖事件添加/删除方法。
  2. 保留这些事件处理程序的列表。
  3. 需要时,将它们全部清除并重新添加其他的。

这可能不起作用或不是最有效的方法,但应该可以完成工作。

于 2008-10-08T15:29:04.330 回答
2

如果您希望能够控制取消订阅,那么您需要按照您接受的答案中指示的路线进行。但是,如果您只是关心在订阅类超出范围时清除引用,那么还有另一个(稍微复杂的)解决方案涉及使用弱引用。我刚刚发布了一个关于这个主题的问题和答案。

于 2009-11-17T08:05:49.073 回答
2

一个简单的解决方案:

只需将事件句柄变量作为参数传递给自身。如果您遇到由于多线程而无法访问原始创建的变量的情况,您可以使用以下方法:

MyEventHandler foo = null;
foo = (s, ev, mehi) => MyMethod(s, ev, foo);
MyEvent += foo;

void MyMethod(object s, MyEventArgs ev, MyEventHandler myEventHandlerInstance)
{
    MyEvent -= myEventHandlerInstance;
    Console.WriteLine("I did it!");
}
于 2014-07-12T14:11:21.187 回答
0

如果你想用这个委托引用一些对象,可能你可以使用 Delegate.CreateDelegate(Type, Object target, MethodInfo methodInfo) .net 考虑委托等于 target 和 methodInfo

于 2014-01-21T03:19:30.787 回答
0

如果最好的方法是在订阅的 eventHandler 上保留一个引用,这可以使用 Dictionary 来实现。

在此示例中,我必须使用匿名方法来包含一组 DataGridView 的 mergeColumn 参数。

使用 MergeColumn 方法并将 enable 参数设置为 true 会启用事件,而使用 false 会禁用它。

static Dictionary<DataGridView, PaintEventHandler> subscriptions = new Dictionary<DataGridView, PaintEventHandler>();

public static void MergeColumns(this DataGridView dg, bool enable, params ColumnGroup[] mergedColumns) {

    if(enable) {
        subscriptions[dg] = (s, e) => Dg_Paint(s, e, mergedColumns);
        dg.Paint += subscriptions[dg];
    }
    else {
        if(subscriptions.ContainsKey(dg)) {
            dg.Paint -= subscriptions[dg];
            subscriptions.Remove(dg);
        }
    }
}
于 2016-10-13T06:40:33.820 回答
0

有一种方法可以通过自己实现闭包而不是 lambda 表达式来解决这个问题。

假设用作捕获变量的类如下。

public class A
{
    public void DoSomething()
    {
        ...
    }
}

public class B
{
    public void DoSomething()
    {
        ...
    }
}

public class C
{
    public void DoSomething()
    {
        ...
    }
}

这些类将用作捕获变量,因此我们将它们实例化。

A a = new A();
B b = new B();
C c = new C();

实现闭包类,如下所示。

private class EventHandlerClosure
{
    public A a;
    public B b;
    public C c;

    public event EventHandler Finished;

    public void MyMethod(object, MyEventArgs args)
    {
        a.DoSomething();
        b.DoSomething();
        c.DoSomething();
        Console.WriteLine("I did it!");

        Finished?.Invoke(this, EventArgs.Empty);
    }
}

实例化闭包类,创建一个处理程序,然后订阅事件并订阅取消订阅闭包类的 Finished 事件的 lambda 表达式。

var closure = new EventHandlerClosure
{
    a = a,
    b = b,
    c = c
};
var handler = new MyEventHandler(closure.MyMethod);
MyEvent += handler;
closure.Finished += (s, e)
{
    MyEvent -= handler;
}
于 2021-04-10T22:23:32.330 回答
0

我最近为一个 C# 项目发现了这个相当古老的线程,并发现所有答案都非常有用。但是,有一个方面不适用于我的特定用例 - 它们都将取消订阅事件的负担放在了订阅者身上。我知道有人可能会说这是订户的工作来处理这个问题,但这对我的项目来说是不现实的。

我对事件的主要用例是监听计时器以对动画进行排序(这是一个游戏)。在这种情况下,我使用大量匿名委托将序列链接在一​​起。存储对这些的引用不是很实用。

为了解决这个问题,我围绕一个事件创建了一个包装类,让您可以订阅单个调用。

internal class EventWrapper<TEventArgs> {
    
    private event EventHandler<TEventArgs> Event;
    private readonly HashSet<EventHandler<TEventArgs>> _subscribeOnces;
    
    internal EventWrapper() {
        _subscribeOnces = new HashSet<EventHandler<TEventArgs>>();
    }

    internal void Subscribe(EventHandler<TEventArgs> eventHandler) {
        Event += eventHandler;
    }

    internal void SubscribeOnce(EventHandler<TEventArgs> eventHandler) {
        _subscribeOnces.Add(eventHandler);
        Event += eventHandler;
    }

    internal void Unsubscribe(EventHandler<TEventArgs> eventHandler) {
        Event -= eventHandler;
    }

    internal void UnsubscribeAll() {
        foreach (EventHandler<TEventArgs> eventHandler in Event?.GetInvocationList()) {
            Event -= eventHandler;
        }
    }

    internal void Invoke(Object sender, TEventArgs e) {
        Event?.Invoke(sender, e);
        if(_subscribeOnces.Count > 0) {
            foreach (EventHandler<TEventArgs> eventHandler in _subscribeOnces) {
                Event -= eventHandler;
            }
            _subscribeOnces.Clear();
        }
    }

    internal void Remove() {
        UnsubscribeAll();
        _subscribeOnces.Clear();
    }
}

在类中使用它的另一个好处是您可以将其设为私有并仅公开您想要的功能。例如,仅公开 SubscribeOnce(而不是订阅)方法。

public class MyClass {
    
    private EventWrapper<MyEventEventArgs> myEvent = new EventWrapper<MyEventEventArgs>();
    
    public void FireMyEvent() {
        myEvent.Invoke(this, new MyEventEventArgs(1000, DateTime.Now));
    }
    
    public void SubscribeOnce(EventHandler<MyEventEventArgs> eventHandler) {
        myEvent.SubscribeOnce(eventHandler);
    }
    
    public class MyEventEventArgs : EventArgs {
        public int MyInt;
        public DateTime MyDateTime;
        
        public MyEventEventArgs(int myInt, DateTime myDateTime) {
            MyInt = myInt;
            MyDateTime = myDateTime;
        }
    }
}

这里的权衡是为每个事件创建一个实例的开销更大,但是在我的场景中 - 这是一个可接受的权衡,以确保有效地收集垃圾并且代码在订阅者端更易于维护。 完整的例子在这里

于 2021-07-12T01:12:14.007 回答