与lambdas一起使用的弱事件处理程序模型

Ben*_*jol 38 .net c# events lambda memory-leaks

好吧,所以这更像是一个答案而不是一个问题,但在提出这个问题之后,将Dustin Campbell,Egor的各个部分以及" IObservable/Rx/Reactive框架 "中的最后一个提示拉到一起,我想我为这个特殊问题制定了可行的解决方案.它可能会被IObservable/Rx/Reactive框架完全取代,但只有经验才会显示出来.

我故意创造了一个新问题,给我空间来解释我如何得到这个解决方案,因为它可能不会立即显而易见.

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

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

Ben*_*jol 34

'答案

(如果您想了解我如何获得此解决方案,请阅读以下内容)

使用,给定带有vanilla 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
Run Code Online (Sandbox Code Playgroud)

(*这是Rx的解决方法)

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

注意:在某些情况下,这个CAN会在内存中引用为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);
}
Run Code Online (Sandbox Code Playgroud)

细节

我的出发点是Egor的优秀答案(请参阅带注释的版本链接):

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;
}
Run Code Online (Sandbox Code Playgroud)

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

我摆弄着并设法提出这个通用的解决方案:

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);
}
Run Code Online (Sandbox Code Playgroud)

然而,该解决方案的问题在于它只是通用的,它无法处理标准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);
}
Run Code Online (Sandbox Code Playgroud)

但是,正如我在这里暗示的那样,这将无法编译,因为没有办法将T限制为委托.

那时,我几乎放弃了.尝试与C#规范作斗争是没有意义的.

但是,昨天,我从Reactive框架中发现了Observable.FromEvent方法,我没有实现,但是用法似乎有点熟悉,而且非常有趣:

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

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

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

事后

我完全建议花时间了解反应框架(或最终被调用的内容).它非常有趣,而且有点令人费解.我怀疑它也会使这样的问题变得多余.

到目前为止,我见过的最有趣的东西是Channel9上的视频.


Mar*_*ark 5

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

Action<int,int> myDelegate = new Action<int,int>( aMethodOnMyClass );
myDelegate.MakeWeak();
Run Code Online (Sandbox Code Playgroud)

就这么简单!