用于 lambda 的弱事件处理程序模型

Posted

技术标签:

【中文标题】用于 lambda 的弱事件处理程序模型【英文标题】:Weak event handler model for use with lambdas 【发布时间】:2010-12-17 08:41:46 【问题描述】:

好的,所以这更像是一个答案而不是一个问题,但是在询问了this question,并将来自Dustin Campbell、Egor 的各个位汇总在一起,以及来自“IObservable/Rx/Reactive framework”的最后一个提示之后,我想我已经为这个特定问题制定了一个可行的解决方案。它可能会被 IObservable/Rx/Reactive 框架完全取代,但只有经验会证明这一点。

我特意提出了一个新问题,给我空间来解释我是如何得到这个解决方案的,因为它可能不会立即显而易见。

有很多相关的问题,大多数告诉你如果你想以后能够分离它们,你不能使用内联 lambda:

Weak events in .Net? Unhooking events with lambdas in C# Can using lambdas as event handlers cause a memory leak? How to unsubscribe from an event which uses a lambda expression? Unsubscribe anonymous method in C#

确实,如果希望以后能够分离它们,你需要保留对你的 lambda 的引用。但是,如果您只是希望事件处理程序在订阅者超出范围时自行分离,那么这个答案适合您。

【问题讨论】:

开源项目Sharp Observation(在codeplex上)提供了一个非常好的通用弱事件/委托实现,它在语法上令人愉悦且易于使用。 sharpobservation.codeplex.com 除了引用的链接之外,这个“问题”中没有一个“问题” 【参考方案1】:

“答案”

(如果您想了解我是如何得到这个解决方案的,请阅读下文)

用法,给定一个带有普通MouseDown 事件和特定EventHandler<ValueEventArgs> ValueEvent 事件的控件:

// for 'vanilla' events
SetAnyHandler<Subscriber, MouseEventHandler, MouseEventArgs>(
    h => (o,e) => h(o,e), //don't ask me, but it works*.
    h => control.MouseDown += h,
    h => control.MouseDown -= h,
    subscriber,
    (s, e) => s.DoSomething(e));  //**See note below

// for generic events
SetAnyHandler<Subscriber, ValueEventArgs>(
    h => control.ValueEvent += h,
    h => control.ValueEvent -= h,
    subscriber,
    (s, e) => s.DoSomething(e));  //**See note below

(*这是来自Rx 的解决方法)

(** 避免在此处直接调用订阅者对象很重要(例如,如果我们在订阅者类中,则放置subscriber.DoSomething(e) 或直接调用DoSomething(e)。这样做有效地创建了对订阅者,这完全击败了对象...)

注意:在某些情况下,这可以在内存中留下对为 lambdas 创建的包装类的引用,但它们只对字节进行加权,所以我不会太在意。

实施:

//This overload handles any type of EventHandler
public static void SetAnyHandler<S, TDelegate, TArgs>(
    Func<EventHandler<TArgs>, TDelegate> converter, 
    Action<TDelegate> add, Action<TDelegate> remove,
    S subscriber, Action<S, TArgs> action)
    where TArgs : EventArgs
    where TDelegate : class
    where S : class

    var subs_weak_ref = new WeakReference(subscriber);
    TDelegate handler = null;
    handler = converter(new EventHandler<TArgs>(
        (s, e) =>
        
            var subs_strong_ref = subs_weak_ref.Target as S;
            if(subs_strong_ref != null)
            
                action(subs_strong_ref, e);
            
            else
            
                remove(handler);
                handler = null;
            
        ));
    add(handler);


// this overload is simplified for generic EventHandlers
public static void SetAnyHandler<S, TArgs>(
    Action<EventHandler<TArgs>> add, Action<EventHandler<TArgs>> remove,
    S subscriber, Action<S, TArgs> action)
    where TArgs : EventArgs
    where S : class

    SetAnyHandler<S, EventHandler<TArgs>, TArgs>(
        h => h, add, remove, subscriber, action);

细节

我的出发点是Egor 的出色回答(有关带有 cmets 的版本,请参阅链接):

public static void Link(Publisher publisher, Control subscriber) 
    var subscriber_weak_ref = new WeakReference(subscriber);
    EventHandler<ValueEventArgs<bool>> handler = null;
    handler = delegate(object sender, ValueEventArgs<bool> e) 
            var subscriber_strong_ref = subscriber_weak_ref.Target as Control;
            if (subscriber_strong_ref != null) subscriber_strong_ref.Enabled = e.Value;
            else 
                    ((Publisher)sender).EnabledChanged -= handler;
                    handler = null; 
            
    ;

    publisher.EnabledChanged += handler;

困扰我的是事件被硬编码到方法中。所以这意味着对于每个新事件,都有一个新方法可以编写。

我摸索着想出了这个通用的解决方案:

private static void SetAnyGenericHandler<S, T>(
     Action<EventHandler<T>> add,     //to add event listener to publisher
     Action<EventHandler<T>> remove,  //to remove event listener from publisher
     S subscriber,                    //ref to subscriber (to pass to action)
     Action<S, T> action)             //called when event is raised
    where T : EventArgs
    where S : class

    var subscriber_weak_ref = new WeakReference(subscriber);
    EventHandler<T> handler = null;
    handler = delegate(object sender, T e)
    
        var subscriber_strong_ref = subscriber_weak_ref.Target as S;
        if(subscriber_strong_ref != null)
        
            Console.WriteLine("New event received by subscriber");
            action(subscriber_strong_ref, e);
        
        else
        
            remove(handler);
            handler = null;
        
    ;
    add(handler);

但是该解决方案的问题在于它只是通用的,它无法处理标准的 winforms MouseUp、MouseDown 等...

所以我试着让它更通用:

private static void SetAnyHandler<T, R>(
    Action<T> add,      //to add event listener to publisher
    Action<T> remove,   //to remove event listener from publisher
    Subscriber subscriber,  //ref to subscriber (to pass to action)
    Action<Subscriber, R> action) 
    where T : class

    var subscriber_weak_ref = new WeakReference(subscriber);
    T handler = null;
    handler = delegate(object sender, R e) //<-compiler doesn't like this line
    
        var subscriber_strong_ref = subscriber_weak_ref.Target as Subscriber;
        if(subscriber_strong_ref != null)
        
            action(subscriber_strong_ref, e);
        
        else
        
            remove(handler);
            handler = null;
        
    ;
    remove(handler);

但是,正如我所暗示的 here,这不会编译,因为无法将 T 限制为代表。

那时,我几乎放弃了。试图与 C# 规范抗争是没有意义的。

不过,昨天我从Reactive框架中发现了Observable.FromEvent方法,我没有实现,但是用法好像有点熟悉,很有趣:

var mousedown = Observable.FromEvent<MouseEventHandler, MouseDownEventArgs>(
      h => new MouseEventHandler(h),
      h => control.MouseDown += h,
      h => control.MouseDown -= h);

这是引起我注意的第一个论点。这是缺少委托类型约束的解决方法。我们通过传入将创建委托的函数来获取它。

将所有这些放在一起为我们提供了此答案顶部显示的解决方案。

事后考虑

我完全建议花时间了解反应式框架(或任何它最终被称为的框架)。这很有趣,而且有点令人兴奋。我怀疑它也会使这样的问题变得完全多余。

到目前为止,我看到的最有趣的东西是Channel9 上的视频。

【讨论】:

是的,我知道,我真的应该开一个技术博客,但我很少有什么有趣的东西可说.. 您正在探索的领域远远超出了我目前的 .NET 和 C# 能力水平,但它很有趣,而且发人深省。感谢您花时间详细发布!恕我直言,这种突变的问答“dynamic-duo”应该是一种特殊的***“对象”(“元评论”?)。也许适合 CodeProject 文章?最好的, 大多数人没有意识到您在这里谈论的内容有多么重要。干的很好。谢谢 @Benjol 不客气,但我认为我仍然更喜欢 Dustin Campbell 在diditwith.net/2007/03/23/… 的解决方案,尽管就使用模式的可读性而言。 SO 上有一个解决方案,它也可以与其他 EventHandler 委托类型一起使用:***.com/questions/1089309/weak-events-in-net 我认为您不需要将 T 约束到 Delegate,而是将 T 约束到类,然后将 T 用作 Delegate。【参考方案2】:

如果您前往 CodePlex,有一个名为 Sharp Observation 的项目,作者在其中构建了一个很好的弱委托提供程序,并在 MSIL 中实现。快速、灵活、易于使用: 例如

Action<int,int> myDelegate = new Action<int,int>( aMethodOnMyClass );
myDelegate.MakeWeak();

就这么简单!

【讨论】:

有趣的库,但它的许可证是什么? 作为后续:WeakDelegates 在版本 1.1.700 中从 SharpObservation 中删除 (nuget.org/packages/SharpObservation)【参考方案3】:

我长期以来一直在寻找解决方案,并且大多数人都使用讨厌的反射,但 Benjohl 的回答很棒。我对其进行了调整以添加对非通用 EventHandler 的支持,DependencyPropertyChangedEventArgs 不继承自 EventArgs,并允许您自己手动注销事件。我会对人们的想法非常感兴趣,尤其是 Benjohl。

/// <summary>
/// Weakly registers for events using <see cref="WeakReference"/>.
/// </summary>
public sealed class WeakEvent

    private Action removeEventHandler;

    /// <summary>
    /// Initializes a new instance of the <see cref="WeakEvent"/> class.
    /// </summary>
    /// <param name="removeEventHandler">The remove event handler function.</param>
    private WeakEvent(Action removeEventHandler)
    
        this.removeEventHandler = removeEventHandler;
    

    /// <summary>
    /// Weakly registers the specified subscriber to the the given event of type 
    /// <see cref="EventHandler"/>.
    /// </summary>
    /// <example>
    /// Application application;
    /// WeakEvent.RegisterTextBox, TextChangedEventArgs>(
    ///     this,
    ///     eventHandler => textBox.TextChanged += eventHandler,
    ///     eventHandler => textBox.TextChanged -= eventHandler,
    ///     (sender, e) => this.OnTextChanged(sender, e));
    /// </example>
    /// <typeparam name="S">The type of the subscriber.</typeparam>
    /// <param name="subscriber">The subscriber.</param>
    /// <param name="addEventhandler">The add eventhandler.</param>
    /// <param name="removeEventHandler">The remove event handler function.</param>
    /// <param name="action">The event execution function.</param>
    public static WeakEvent Register<S>(
        S subscriber,
        Action<EventHandler> addEventhandler,
        Action<EventHandler> removeEventHandler,
        Action<S, EventArgs> action)
        where S : class
    
        return Register<S, EventHandler, EventArgs>(
            subscriber,
            eventHandler => (sender, e) => eventHandler(sender, e),
            addEventhandler,
            removeEventHandler,
            action);
    

    /// <summary>
    /// Weakly registers the specified subscriber to the the given event of type 
    /// <see cref="EventHandlerT"/>.
    /// </summary>
    /// <example>
    /// Application application;
    /// WeakEvent.RegisterTextBox, TextChangedEventArgs>(
    ///     this,
    ///     eventHandler => textBox.TextChanged += eventHandler,
    ///     eventHandler => textBox.TextChanged -= eventHandler,
    ///     (sender, e) => this.OnTextChanged(sender, e));
    /// </example>
    /// <typeparam name="S">The type of the subscriber.</typeparam>
    /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
    /// <param name="subscriber">The subscriber.</param>
    /// <param name="addEventhandler">The add eventhandler.</param>
    /// <param name="removeEventHandler">The remove event handler function.</param>
    /// <param name="action">The event execution function.</param>
    public static WeakEvent Register<S, TEventArgs>(
        S subscriber, 
        Action<EventHandler<TEventArgs>> addEventhandler, 
        Action<EventHandler<TEventArgs>> removeEventHandler,
        Action<S, TEventArgs> action)
        where S : class
        where TEventArgs : EventArgs
    
        return Register<S, EventHandler<TEventArgs>, TEventArgs>(
            subscriber,
            eventHandler => eventHandler, 
            addEventhandler, 
            removeEventHandler, 
            action);
    

    /// <summary>
    /// Weakly registers the specified subscriber to the the given event.
    /// </summary>
    /// <example>
    /// TextBox textbox;
    /// WeakEvent.RegisterTextBox, TextChangedEventHandler, TextChangedEventArgs>(
    ///     this,
    ///     eventHandler => (sender, e) => eventHandler(sender, e),
    ///     eventHandler => textBox.TextChanged += eventHandler,
    ///     eventHandler => textBox.TextChanged -= eventHandler,
    ///     (sender, e) => this.OnTextChanged(sender, e));
    /// </example>
    /// <typeparam name="S">The type of the subscriber.</typeparam>
    /// <typeparam name="TEventHandler">The type of the event handler.</typeparam>
    /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
    /// <param name="subscriber">The subscriber.</param>
    /// <param name="getEventHandler">The get event handler function.</param>
    /// <param name="addEventHandler">The add event handler function.</param>
    /// <param name="removeEventHandler">The remove event handler function.</param>
    /// <param name="action">The event execution function.</param>
    public static WeakEvent Register<S, TEventHandler, TEventArgs>(
        S subscriber, 
        Func<EventHandler<TEventArgs>, TEventHandler> getEventHandler,
        Action<TEventHandler> addEventHandler, 
        Action<TEventHandler> removeEventHandler,
        Action<S, TEventArgs> action)
        where S : class
        where TEventHandler : class
        where TEventArgs : EventArgs

    
        WeakReference weakReference = new WeakReference(subscriber);

        TEventHandler eventHandler = null;
        eventHandler = getEventHandler(
            new EventHandler<TEventArgs>(
                (sender, e) =>
                
                    S subscriberStrongRef = weakReference.Target as S;

                    if (subscriberStrongRef != null)
                    
                        action(subscriberStrongRef, e);
                    
                    else
                    
                        removeEventHandler(eventHandler);
                        eventHandler = null;
                    
                ));

        addEventHandler(eventHandler);

        return new WeakEvent(() => removeEventHandler(eventHandler));
    

    public static WeakEvent Register<S>(
        S subscriber,
        Action<DependencyPropertyChangedEventHandler> addEventHandler,
        Action<DependencyPropertyChangedEventHandler> removeEventHandler,
        Action<S, DependencyPropertyChangedEventArgs> action)
        where S : class
    
        WeakReference weakReference = new WeakReference(subscriber);

        DependencyPropertyChangedEventHandler eventHandler = null;
        eventHandler = new DependencyPropertyChangedEventHandler(
            (sender, e) =>
            
                S subscriberStrongRef = weakReference.Target as S;

                if (subscriberStrongRef != null)
                
                    action(subscriberStrongRef, e);
                
                else
                
                    removeEventHandler(eventHandler);
                    eventHandler = null;
                
            );

        addEventHandler(eventHandler);

        return new WeakEvent(() => removeEventHandler(eventHandler));
    

    /// <summary>
    /// Manually unregisters this instance from the event.
    /// </summary>
    public void Unregister()
    
        if (this.removeEventHandler != null)
        
            this.removeEventHandler();
            this.removeEventHandler = null;
        
    

【讨论】:

有趣,我刚刚回到这个问题,因为我实际上正在考虑在生产中使用我的代码。感谢您的反馈,我很抱歉承认我对您的柚木没什么好说的,除非它对您有用,我很高兴! 您的评论有误。最后一个参数:(sender, e) =&gt; this.OnTextChanged(sender, e));。它应该是订阅者而不是发送者,您应该调用(subscriber, e) =&gt; subscriber.OnTextChanged(sender, e));【参考方案4】:

Dustin Campbell 的方法已经非常出色了。剩下的唯一一件事是,保存集成到 .NET 中的解决方案,这是一种创建真正通用的弱事件处理程序的非常简单的方法:

http://puremsil.wordpress.com/2010/05/03/generic-weak-event-handlers/

【讨论】:

以上是关于用于 lambda 的弱事件处理程序模型的主要内容,如果未能解决你的问题,请参考以下文章

使用 lambda 表达式时删除附加的事件处理程序 [重复]

作为 SQS 事件处理程序的 Lambda 函数未运行部分代码并成功完成

如何修复由 lambda 事件处理程序引起的 GC 周期?

JavaFX从lambda表达式中删除事件处理程序

C#入门详解笔记事件 泛型 委托 Lambda表达式 LINQ

使用 MVVM 帮助器的视图模型中的 Xamarin 事件处理程序