在 C# 事件中取消订阅匿名方法

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

如果我订阅了这样的事件 Event :

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


MyEvent += MyMethod;

我可以像这样取消订阅事件:

MyEvent -= MyMethod;

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

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

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

84279 次浏览

从记忆中来看,当涉及到用匿名方法创建的委托的等价性时,规范明确地没有保证行为的任何一种方式。

如果您需要退订,您应该使用“正常”方法或将委托保留在其他地方,这样您就可以使用与订阅时完全相同的委托退订。

有点蹩脚的方法:

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. 当需要时,清除它们并重新添加其他的。

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

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


MyEvent += myDelegate;




// .... later


MyEvent -= myDelegate;

只要保持对委托的引用。

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

例子:

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

在3.0中可以缩短为:

MyHandler myDelegate = ()=>Console.WriteLine("I did it!");
MyEvent += myDelegate;
...
MyEvent -= myDelegate;

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

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

我已经写了一个完整的帖子关于这个技巧在这里

如果你想用这个委托引用某个对象,你可以使用委托。创建elegate(类型,对象目标,MethodInfo MethodInfo) .net考虑委托等于由目标和methodInfo

一个简单的解决办法:

只是将eventandle变量作为参数传递给自身。 事件,如果你的情况下,你不能访问原始创建的变量,因为多线程,你可以使用这个:

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!");
}

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

在本例中,我必须使用匿名方法为一组DataGridViews包含mergeColumn参数。

使用启用参数设置为true的MergeColumn方法启用事件,而使用设置为false的MergeColumn方法禁用事件。

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

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

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

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

有一种方法可以通过自己实现闭包而不是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;
}

我最近在一个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;
}
}
}

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

这里有一个简单的解决方案,它删除所有指定的方法从一个事件。还有匿名方法。

使用这段代码并调整名称。

if (MyEvent != null)
foreach (Delegate del in MyEvent.GetInvocationList())
MyEvent -= (EventHandler<MyEventHandlerType>)del;

示例使用

public class SomeClass
{
public event EventHandler<NiceEventArgs> NiceEvent;


public void RemoveHandlers()
{
if (NiceEvent != null)
foreach (Delegate del in NiceEvent.GetInvocationList())
NiceEvent -= (EventHandler<NiceEventArgs>)del;
}
}

感谢hemme的回答,我用它作为灵感。