单次事件订阅

13

我相当确信这是不可能的,但我仍会提出问题。

为了对事件进行一次性订阅,我经常使用这个(我自己发明的)模式:

EventHandler handler=null;
handler = (sender, e) =>
{
    SomeEvent -= handler;
    Initialize();
};
SomeEvent += handler;

这段代码有很多样板内容,并且会让 Resharper 抱怨修改了闭包。有没有办法将此模式转换为扩展方法或类似的方式?有更好的实现方式吗?

理想情况下,我希望像这样:

SomeEvent.OneShot(handler)

我认为你完全可以把样板代码放到扩展方法中,难道我理解错了问题吗? - Achim
@spender:所有委托都是引用类型。 - Ben Voigt
是的,我刚刚明白了...但是要传递一个事件,你必须传递一个副本。 - spender
你说得对。事件处理并不容易。我会考虑的。;-) - Achim
说实话,在我的代码中很少使用事件,但在这种情况下(并且经过检查,我使用此模式的所有其他时间),我被困在一个框架事件中,我只需要捕获第一次触发。 - spender
显示剩余2条评论
3个回答

4
很难将代码重构为扩展方法,因为在C#中引用事件的唯一方式是订阅( + = )或取消订阅( - = )它(除非它在当前类中声明)。您可以使用与响应式扩展相同的方法:Observable.FromEvent需要两个委托来订阅和取消订阅事件。因此,您可以这样做:
public static class EventHelper
{
    public static void SubscribeOneShot(
        Action<EventHandler> subscribe,
        Action<EventHandler> unsubscribe,
        EventHandler handler)
    {
        EventHandler actualHandler = null;
        actualHandler = (sender, e) =>
        {
            unsubscribe(actualHandler);
            handler(sender, e);
        };
        subscribe(actualHandler);
    }
}

...

Foo f = new Foo();
EventHelper.SubscribeOneShot(
    handler => f.Bar += handler,
    handler => f.Bar -= handler,
    (sender, e) => { /* whatever */ });

尝试不错,但是通过委托订阅/取消订阅的方式,需要编写相同数量的样板代码,而且使用一个不熟悉的接口,在第一次阅读时并不明显。用户提供的委托中也没有强制执行订阅/取消订阅,这意味着很容易出现错误,并且我对方法名称感到担忧。谢谢,但我会坚持使用我的方法 :) - spender
我认为这是朝着正确方向迈出的一步。毕竟,方法名称清楚地表明了代码的意图,并且需要输入的代码稍微少了一些。这肯定是一个开始。 - Jeff Yates
1
+1,无论如何你都不能将 f.Bar += / -= handler 抽象化到调用者中,而订阅/移除操作在扩展方法中执行而不是处理程序中被认为是一种特性,而不是错误 :) - Frédéric Hamidi
1
@spender,是的,它仍然相当丑陋...但我想不到更好的方法。不幸的是,该语言缺少一些功能,这使得它更容易(具体来说,事件作为第一类公民)... @Jeff Yates,实际上代码并没有少多少,但它是更简单的代码,更少出错。 - Thomas Levesque

1
以下代码对我来说是有效的。虽然必须通过字符串指定事件不是很完美,但我不知道如何解决这个问题。我猜在当前的C#版本中可能不可能实现。
using System;
using System.Reflection;

namespace TestProject
{
    public delegate void MyEventHandler(object sender, EventArgs e);

    public class MyClass
    {
        public event MyEventHandler MyEvent;

        public void TriggerMyEvent()
        {
            if (MyEvent != null)
            {
                MyEvent(null, null);
            }
            else
            {
                Console.WriteLine("No event handler registered.");
            }
        }
    }

    public static class MyExt
    {
        public static void OneShot<TA>(this TA instance, string eventName, MyEventHandler handler)
        {
            EventInfo i = typeof (TA).GetEvent(eventName);
            MyEventHandler newHandler = null;
            newHandler = (sender, e) =>
                             {
                                 handler(sender, e);
                                 i.RemoveEventHandler(instance, newHandler);
                             };
            i.AddEventHandler(instance, newHandler);
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            MyClass c = new MyClass();
            c.OneShot("MyEvent",(sender,e) => Console.WriteLine("Handler executed."));
            c.TriggerMyEvent();
            c.TriggerMyEvent();
        }
    }
}

刚刚发现,自定义事件访问器也可能是一个选项。但这取决于您的偏好和要求。 - Achim

1
我建议使用“自定义”事件,这样您就可以访问调用列表,并通过使用Interlocked.Exchange来同时读取和清除调用列表来引发事件。如果需要,在线程安全的方式下,可以使用简单的链表堆栈来进行事件订阅/取消订阅/引发;当事件被引发后,代码可以在Interlocked.Exchange之后反转堆栈项的顺序。对于取消订阅方法,我可能会建议在调用列表项中简单地设置一个标志。理论上,如果事件重复订阅和取消订阅而事件从未被引发,这可能导致内存泄漏,但它将使取消订阅方法非常容易实现线程安全。如果想避免内存泄漏,可以保持未订阅事件的计数;如果在尝试添加新事件时列表中有太多未订阅事件,添加方法可以遍历列表并将其删除。这仍然适用于完全无锁的线程安全代码,但更加复杂。

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