一次性泛型事件调用

本文关键字:调用 事件 泛型 一次性 | 更新日期: 2023-09-27 18:36:03

我试图实现的是创建方便的语法,以便只调用一次事件的回调。

public static class Extensions
{
    public static void Once<T>(this EventHandler<T> @event, EventHandler<T> callback)
    {
        EventHandler<T> cb = null;
        cb = (object that, T info) => {
            if (callback != null) {
                callback(that, info);
            }
            @event -= cb;
        };
        @event += cb;
    }
}

这应该允许我们写这样的东西:

obj.OnSomething.Once((sender, obj) => Console.WriteLine(obj));

请注意,OnSomething 是某种类型的事件 EventHandler。

问题是我收到错误消息:

错误 CS0070:事件SharpTox.Core.Tox.OnFriendMessage' can only appear on the left hand side of += or -= when used outside of the type SharpTox.Core.Tox' (CS0070) (SharpTox.Tests)

难道就没有办法那么容易地做到这一点吗?我是否必须从 OnSomething 中删除事件并使其成为简单的委托?

一次性泛型事件调用

不幸的是,你在这里不能有一个很好的语法。就像错误消息所说的那样,您只能从其定义类外部对事件字段执行+=-=的左侧引用它。

下面是 Rx 用于将可观察量绑定到事件的方法:

var observable = Observable.FromEventPattern(
    handler => obj.OnSomething += handler,
    handler => obj.OnSomething -= handler);

基本上,FromEventPattern是一个帮助程序,它接受两个 lambda:一个用于订阅,另一个用于取消订阅。您可以使用类似的模式,或者只使用 Rx 来实现相同的效果:

Observable.FromEventPattern(h => obj.OnSomething += h, h => obj.OnSomething -= h)
          .Take(1)
          .Subscribe(e => ...);

附带说明一下,这将保留对Subscribe中使用的 lambda 中obj的引用(有中间胶水对象,但这无关紧要)。这意味着,如果事件从未被调用,并且 lambda 是长期存在的,则obj将不符合 GC(这种情况称为事件内存泄漏)。对于您的情况来说,这可能是也可能不是问题。

另一种方法是返回回调。在扩展方法中,回调被删除/删除。这种方法的缺点仍然是事件处理程序函数需要单独定义,并且不能是 lambda。

扩展方法:

public static class Extensions
{   
    public static EventHandler<T> Once<T>(this Action<Object, T> callback)
    {
        return (Object s, T e) => 
        {
            if (callback != null) {
                callback(s, e);
                callback = null;
            }
        };
    }
}

具有不同事件的演示类:

public class Demo
{
    public event EventHandler<String> StringEvent = null;
    public event EventHandler<Int32> IntEvent = null;
    public void NotifyOnWork()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(i);
            if (this.StringEvent != null) { this.StringEvent(this, i.ToString()); }
            if (this.IntEvent != null) { this.IntEvent(this, i); }
        }
    }
}

演示类的用法:

var demo = new Demo();
Action<Object, String> handlerString = (s, e) =>  { Console.WriteLine("echo string {0}", e); };
Action<Object, Int32> handlerInt = (s, e) =>  { Console.WriteLine("echo int {0}", e); };
demo.StringEvent += handlerString.Once();
demo.IntEvent += handlerInt.Once();
demo.StringEvent += (s, e) => Console.WriteLine("i = {0}", e); 
demo.NotifyOnWork();

输出为:

0
echo string 0
i = 0
echo int 0
1
i = 1
2
i = 2
3
i = 3
4
i = 4

您在此处发布了类似问题的答案。

语法看起来有点像这样:

obj.Event += (s, e) =>
{
    Detach(s, nameof(obj.Event));
    // ..do stuff..
};

Detach方法如下所示,可以放在您喜欢的任何位置(很可能是静态帮助程序类):

public static void Detach(object obj, string eventName)
{
    var caller = new StackTrace().GetFrame(1).GetMethod();
    var type = obj.GetType();
    foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
    {
        if (typeof(Delegate).IsAssignableFrom(field.FieldType))
        {
            var handler = (field.GetValue(obj) as Delegate)?.GetInvocationList().FirstOrDefault(m => m.Method.Equals(caller));
            if (handler != null)
            {
                type.GetEvent(eventName).RemoveEventHandler(obj, handler);
                return;
            }
        }
    }
}