如何使用 lambda 表达式创建新的 EventHandler?

8

看起来我应该已经拥有所需的一切,但是实现它的细节让我抓狂。

我有一个静态工具方法,它接受一个Web服务客户端对象,从中提取指定的EventInfo,并应该向该事件添加一些处理程序,基本上就是Web服务调用完成时的回调。问题在于,由于任何给定的事件本质上都有自己的处理程序重载(WCF生成的代码为每个方法和相应事件提供了一个唯一的SomeMethodCompletedEventArgs),我无法弄清楚如何实现这一点。

我想要附加两个处理程序,第一个只是一个lambda函数:

(obj, args) => task.Complete()

我想做的很简单:

这里是需要翻译的内容。
eventInfo.AddEventHandler(client, new EventHandler((obj, args) => task.Complete()));

然而,这会生成一个运行时的InvalidCastException,因为eventInfo期望的是一个EventHandler<SomeMethodCompletedEventArgs>,而不是一个普通的EventHandler。我认为这意味着我需要使用eventInfo.EventHandlerType动态创建EventHandler委托,但我还没有想出如何将其与lambda函数结合起来,或者以其他方式创建一个真正不关心正在使用哪种EventArgs类型的接收器。
我找到的唯一解决方法是创建一个通用的模板参数,以传递特定的事件参数类型。这使我能够执行以下操作:
eventInfo.AddEventHandler(client, new EventHandler<E>(...));
E是需要考虑的参数。然而,这种方式显然很笨拙,只要提取eventInfo就可以获得我们所需的一切信息,因此这种方式似乎是错误的。
值得注意的是,我正在使用针对Xamarin的受限PCL框架,该框架显然不包括已在相关问题中提到的静态Delegate.CreateDelegate()方法。即便如此,我仍然可以访问Activator,这应该可以处理大部分相同的情况。

请注意,通常不建议将lambda附加到事件处理程序,因为它们只能通过反射的相当复杂的过程来删除。 - Oblivious Sage
1
@ObliviousSage:“它们只能通过反射的相当复杂的过程来删除”--这是不正确的。首先,在许多情况下,根本没有必要删除事件处理程序。其次,在需要的场景中,只需要在某个变量中保留委托实例引用,并使用该值取消订阅处理程序即可。不需要反射,在许多情况下,此变量可以简单地是一个捕获的局部变量。 - Peter Duniho
@PeterDuniho 是的,事件处理程序通常不需要被删除;但这并不意味着你应该假设它不需要被删除。您是正确的,如果您保存了对委托的引用,则可以删除委托,但是纯lambda(例如,myObject.SomeEvent += (a, b) => Foo();)无法在没有先前提到的反射的情况下被删除。使用lambda作为事件处理程序的缺点可以得到缓解,并不意味着这些缺点不存在,也不意味着使用lambda作为事件处理程序是一个好习惯。 - Oblivious Sage
1
@ObliviousSage: “但是一个纯lambda不能被删除,除非先使用之前提到的反射。”-- 我猜你需要定义“纯lambda”。因为在你的例子中,你只需要先将lambda分配给一个变量,然后订阅该变量到事件即可。这并不意味着使用lambda作为事件处理程序在某种程度上是有害的或应该避免。 “也不意味着使用lambda作为事件处理程序是一个好习惯”--虽然它不是一个应该避免的习惯。它在广泛的场景下都能很好地工作。 - Peter Duniho
@ObliviousSage:这是一个公正的观点。就目前而言,只要我能够首先解决这个问题,使用委托实例进行清理应该是相当简单的,正如Peter所指出的那样。 - Metameta
2个回答

7
在您提供的示例中,您应该能够仅删除显式委托构造函数:
eventInfo.AddEventHandler(client, (obj, args) => task.Complete());

通过让C#为您推断委托类型,它应该会创建参数所需的确切委托类型。

如果这不能解决您的具体问题,请提供一个良好、最小的完整的代码示例,可以可靠地重现问题,并清晰、精确地解释为什么上述方法无法帮助。


顺便说一句,根据您发布的少量代码难以确定,但通常不需要显式使用AddEventHandler()方法。通常,类只需公开一个事件,然后使用+=语法订阅事件处理程序。


编辑:

从您的评论中,我了解到您需要符合动态提供的事件签名的API要求。个人认为这种设计有些愚蠢,但我假设您被困在其中,可能是由于Xamarin框架的设计。

严格按照所述目标——即给定一个EventHandler实例,生成一个新的委托实例,其类型与运行时提供的Type实例匹配——以下方法应该适用于您:

static Delegate CreateDelegate(Type type, EventHandler handler)
{
    return (Delegate)type.GetConstructor(new [] { typeof(object), typeof(IntPtr) })
        .Invoke(new [] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });
}

示例用法:

eventInfo.AddEventHandler(client,
    CreateDelegate(eventInfo.EventHandlerType, (obj, args) => task.Complete());

您可以编写上述内容作为扩展方法以简化调用(您没有说明client的类型,因此我只是在示例中将其设置为object):
public static void AddEventHandler(this EventInfo eventInfo, object client, EventHandler handler)
{
        object eventInfoHandler = eventInfo.EventHandlerType
            .GetConstructor(new[] { typeof(object), typeof(IntPtr) })
            .Invoke(new[] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });

        eventInfo.AddEventHandler(client, (Delegate)eventInfoHandler);
}

使用示例:

eventInfo.AddEventHandler(client, (obj, args) => task.Complete());

如果您担心 API 的 AddEventHandler() 方法在某些时候会发生变化,从而导致 C# 编译器选择其实现而不是扩展方法,则可以给扩展方法起一个不同的名称。当然,如果今天就已经发生这种情况(假设只有一个AddEventHandler()方法过载,第二个参数为Delegate),上述方法可以工作,但是......缺乏一个好的最小完整的代码示例,我不能保证它能在您自己的代码中正常工作。使用唯一的名称将保证其工作,但会暴露一些"魔法":)。最后,找到可行解决方案后,我才能搜索 Stack Overflow 查找类似的问题,并找到了这个问题,你可以认为它是重复的:Using reflection to specify the type of a delegate (to attach to an event)?。我决定编辑我的答案,而不仅仅是建议关闭你的问题,因为另一个问题的答案并没有提供我认为优雅或易于使用的示例。

您提供的代码行理想,但会生成编译器异常:“无法将 lambda 表达式转换为 'System.Delegate',因为它不是委托类型。通常,我会使用 += 语法,但这种特殊情况不允许使用。WCF 为每个可调用的异步调用生成一个唯一的 event,但我正在尝试创建一个通用工具,既可以调用该方法,又可以在完成时附加回调。我会尝试获取一个可复现的代码示例。 - Metameta
@Metameta:这意味着AddEventHandler()方法使用Delegate作为参数类型,而不是它最终尝试将参数强制转换为的特定委托类型。所以问题是:您有什么理由相信,在给定一个eventInfo对象的情况下,您(不用管编译器)可以知道期望的委托类型是什么?请参考我的请求,提供一个好的、最小化的、完整的代码示例。只有这样才能真正理解问题并提供适合您的答案。 - Peter Duniho
应该使用的委托类型实际上是通过EventInfo知道的,它具有一个EventHandlerType属性,其中包含委托在运行时应符合的类型。我想做的就是创建代码来响应事件,因为我甚至不关心事件参数;这意味着我只想创建一个提供的类型的处理程序,并使用自己的代码。 - Metameta
@Metameta:你正在使用一种不幸和奇怪的API设计。然而,请查看我上面的编辑,以获取一个应该适用于你场景的替代方法。(顺便说一句,请不要使用外部参考链接来支持问题;问题应该完全自包含,以确保即使外部参考变得过时或完全消失,它仍然有用)。 - Peter Duniho
我想感谢你的勇敢努力;实际上,在我的研究中,我已经找到了你链接的问题,尽管我同意你的实现更加优雅。我的情况与所描述的情况的主要区别(我应该将其编辑到我的问题中)是,在Xamarin/Mono中,我无法访问MethodInfo.MethodHandle及其相关的函数指针。 - Metameta
显示剩余2条评论

1
这并不是很难,但需要一定的代码和一些反射。基本想法是将处理程序包装在一个泛型类中,该类由事件类型参数化。
HandlerFor<T> : IDisposable where T : EventArgs. 

这个类有一个私有的成员函数,其签名与所需的事件处理程序相匹配:
void Handle(object sender, T eventArgs) 

它将在构造时注册,在处理时取消注册,并在事件发生时调用其构造函数中提供的给定 Action
为了隐藏实现细节,并仅公开 IDisposable 作为受控事件处理程序生命周期范围和注销的句柄,我将其包装在一个 EventHandlerRegistry 类中。这也将允许添加未来的改进,例如构建工厂委托而不是重复调用 Activator.CreateInstance() 来构建 HandleFor 实例。
它看起来像这样:
public class EventHandlerRegistry : IDisposable
{
    private ConcurrentDictionary<Type, List<IDisposable>> _registrations;

    public EventHandlerRegistry()
    {
        _registrations = new ConcurrentDictionary<Type, List<IDisposable>>();
    }

    public void RegisterHandlerFor(object target, EventInfo evtInfo, Action eventHandler)
    {
        var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0];
        _registrations.AddOrUpdate(
            evtType,
            (t) => new List<IDisposable>() { ConstructHandler(target, evtType, evtInfo, eventHandler) },
            (t, l) => { l.Add(ConstructHandler(target, evtType, evtInfo, eventHandler)); return l; });
    }

    public IDisposable CreateUnregisteredHandlerFor(object target, EventInfo evtInfo, Action eventHandler)
    {
        var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0];
        return ConstructHandler(target, evtType, evtInfo, eventHandler);
    }

    public void Dispose()
    {
        var regs = Interlocked.Exchange(ref _registrations, null);
        if (regs != null)
        {
            foreach (var reg in regs.SelectMany(r => r.Value))
                reg.Dispose();
        }
    }

    private IDisposable ConstructHandler(object target, Type evtType, EventInfo evtInfo, Action eventHandler)
    {
        var handlerType = typeof(HandlerFor<>).MakeGenericType(evtType);
        return Activator.CreateInstance(handlerType, target, evtInfo, eventHandler) as IDisposable;
    }

    private class HandlerFor<T> : IDisposable where T : EventArgs
    {
        private readonly Action _eventAction;
        private readonly EventInfo _evtInfo;
        private readonly object _target;
        private EventHandler<T> _registeredHandler;

        public HandlerFor(object target, EventInfo evtInfo, Action eventAction)
        {
            _eventAction = eventAction;
            _evtInfo = evtInfo;
            _target = target;
            _registeredHandler = new EventHandler<T>(this.Handle);
            _evtInfo.AddEventHandler(target, _registeredHandler);
        }

        public void Unregister()
        {
            var registered = Interlocked.Exchange(ref _registeredHandler, null);
            if (registered != null)
                // Unregistration is awkward: 
                // doing `RemoveEventHandler(_target, registered);` won't work.
                _evtInfo.RemoveEventHandler(_target, new EventHandler<T>(this.Handle));
        }

        private void Handle(object sender, T EventArgs)
        {
            if (_eventAction != null)
                _eventAction();
        }

        public void Dispose()
        {
            Unregister();
        }
    }
}

它支持以相当透明的方式清晰地添加和删除事件处理程序。注意:这还没有按推荐的方式实现IDisposable。您需要自己添加终结器和Dispose(bool isFinalizing)方法。 以下是其用法示例:
public class MyArgs1 : EventArgs
{
    public string Value1;
}

public class MyEventSource
{
    public event EventHandler<MyArgs1> Args1Event;

    public EventInfo GetEventInfo()
    {
        return this.GetType().GetEvent("Args1Event");
    }

    public void FireOne()
    {
        if (Args1Event != null)
            Args1Event(this, new MyArgs1() { Value1 = "Bla " });
    }
}

class Program
{
    public static void Main(params string[] args)
    {
        var myEventSource = new MyEventSource();
        using (var handlerRegistry = new EventHandlerRegistry())
        {
            handlerRegistry.RegisterHandlerFor(
                myEventSource,
                myEventSource.GetEventInfo(),
                () => Console.WriteLine("Yo there's some kinda event goin on"));
            handlerRegistry.RegisterHandlerFor(
                myEventSource,
                myEventSource.GetEventInfo(),
                () => Console.WriteLine("Yeah dawg let's check it out"));

            myEventSource.FireOne();
        }
        myEventSource.FireOne();
    }
}

运行时,将会输出以下内容:

output


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