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

如果我订阅了这样的活动:

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

MyEvent += MyMethod;

我可以像这样取消订阅:

MyEvent -= MyMethod;

但如果我使用匿名方式订阅:

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

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


当前回答

为了将事件的调用列表返回给调用者,您可以对类进行仪器操作,而不是保持对任何委托的引用。基本上你可以这样写(假设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();

关于这个技巧,我已经写了一篇完整的文章。

其他回答

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

例子:

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

我最近在一个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(而不是Subscribe)方法。

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

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

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

MyEvent += myDelegate;


// .... later

MyEvent -= myDelegate;

只要保持对委托的引用。

自从c# 7.0局部函数特性发布以来,J C建议的方法变得非常简洁。

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

老实说,这里没有一个匿名函数作为变量。但我想在你的例子中使用它的动机可以应用到局部函数中。

有点蹩脚的方法:

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

  ...

  public event Action OnDoSomething
  {
    add {
      _eventList.Add(value);
    }
    remove {
      _eventList.Remove(value);
    }
  }
}

重写事件添加/删除方法。 保留这些事件处理程序的列表。 当需要时,清除它们并重新添加其他的。

这可能行不通,也不是最有效的方法,但应该能把工作完成。