使用匿名委托处理事件时的垃圾回收问题

13

更新

我将此处的各种回答结合起来,形成了一个“权威”的答案,并在新问题中进行了发布。

原始问题

在我的代码中,我有一个事件发布器,它存在于整个应用程序的生命周期内(这里简化为基本要素):

public class Publisher
{
    //ValueEventArgs<T> inherits from EventArgs
    public event EventHandler<ValueEventArgs<bool>> EnabledChanged; 
}

由于这个发布者可以在各个地方使用,我为创建这个小助手类而感到非常高兴,以避免在所有订阅者中重写处理代码:

public static class Linker
{
    public static void Link(Publisher publisher, Control subscriber)
    {
         publisher.EnabledChanged += (s, e) => subscriber.Enabled = e.Value;
    }

    //(Non-lambda version, if you're not comfortable with lambdas)
    public static void Link(Publisher publisher, Control subscriber)
    {
         publisher.EnabledChanged +=
             delegate(object sender, ValueEventArgs<bool> e)
             {
                  subscriber.Enabled = e.Value;
             };
    }
}

在使用较小的机器时,它的表现很好,但偶尔会出现以下问题:

System.ComponentModel.Win32Exception
Not enough storage is available to process this command

事实上,在代码中有一个地方动态创建、添加和删除了订阅控件。鉴于我对垃圾回收等的高级理解(即没有,直到昨天),我从未想过要清理掉自己留下的东西,因为在绝大多数情况下,订阅者也会在应用程序的生命周期内存在。

我已经尝试了一段时间 Dustin Campbell 的 WeakEventHandler,但它 不能与匿名委托一起使用(至少对我来说是这样)。

有没有什么方法可以解决这个问题?我真的很想避免在整个项目中都复制粘贴样板代码。

(哦,不要问我为什么我们总是在创建和销毁控件,那不是我的设计决策……)

(PS:这是一个 Winforms 应用程序,但我们已经升级到 VS2008 和 .Net 3.5,我应该考虑使用 弱事件模式 吗?)

(PPS:来自 Rory 的 好回答,但如果有人能提出一个类似于 WeakEventHandler 的等效方案,避免我必须记住显式 Unlink/Dispose,那就太棒了……)

编辑 我必须承认,我通过 “回收利用” 相关控件来解决这个问题。然而,这个解决方法又让我陷入了麻烦,因为我用的 'key' 显然不是唯一的(哭泣)。我刚刚发现其他链接 这里(尝试过了-看起来有点 脆弱了-GC会清除委托,即使目标还活着,与下面的s,oɔɯǝɹ答案存在同样的问题),这里(强制你修改发布者,并且不能真正地与匿名委托一起使用),以及这里(由Dustin Campbell称为不完整)。

我想到了一个可能在语义上不可能的解决方法——闭包被设计为“即使我消失了也会留下来”。

我找到了另一个解决方法,所以我会使用它,直到有一个 声音从神那里传来


@Benjol 哇.. 很高兴见到另一个 Lambda 狂热者 :-) - chakrit
看看响应式框架是否对这个问题有什么建议会很有趣... - Benjol
4个回答

5

我知道这个问题很古老,但是我找到了它,我认为其他人也可能会找到。我正在尝试解决一个相关的问题,可能有一些见解。

你提到了达斯汀·坎贝尔的WeakEventHandler - 它的设计确实无法使用匿名方法。我试图拼凑一些东西,但意识到a)在99%的情况下,他最初的解决方案更安全,b)在那些少数情况下,我必须(注意:必须,不是“想要因为lambda更漂亮和简洁”)如果你聪明点,也可以让它工作。

你的例子似乎正是这种一次性的情况,通过巧妙地处理可以得出相当简洁的解决方案。


public static class Linker {
    public static void Link(Publisher publisher, Control subscriber) {
        // anonymous method references the subscriber only through weak 
        // references,so its existance doesn't interfere with garbage collection
        var subscriber_weak_ref = new WeakReference(subscriber);

        // this instance variable will stay in memory as long as the  anonymous
        // method holds a reference to it we declare and initialize  it to 
        // reserve the memory (also,  compiler complains about uninitialized
        // variable otherwise)
        EventHandler<ValueEventArgs<bool>> handler = null;

        // when the handler is created it will grab references to the  local 
        // variables used within, keeping them in memory after the function 
        // scope ends
        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 {
                // unsubscribing the delegate from within itself is risky, but
                // because only one instance exists and nobody else has a
                // reference to it we can do this
                ((Publisher)sender).EnabledChanged -= handler;

                // by assigning the original instance variable pointer to null
                // we make sure that nothing else references the anonymous
                // method and it can be collected. After this, the weak
                //  reference and the handler pointer itselfwill be eligible for
                // collection as well.
                handler = null; 
            }
        };

        publisher.EnabledChanged += handler;
    }
}

据传闻,WPF弱事件模式会带来很大的开销,因此在这种特定情况下我不会使用它。此外,在WinForm应用程序中引用核心WPF库似乎也有点过重。


这个问题可能很古老,但它仍然引起了我的兴趣!我已经将你的解决方案添加到我的测试应用程序中,它似乎能够正常工作,所以我很荣幸地授予你第一个绿色勾号! - Benjol
谢谢!您也可以查看我的Dustin Campbell实现版本。我将其发布为对此问题的答案: https://dev59.com/NnNA5IYBdhLWcg3wHp6B#1483010 - Egor
哎呀!这个有点复杂,我得花时间来分析它。我正在酝酿另一个版本... - Benjol

5

如果您在控件从表单中移除时保留了对匿名委托的引用,并且在此之后将其删除,那么这应该允许控件和匿名委托都被垃圾回收。

因此,代码可能类似于以下内容:

public static class Linker
{

    //(Non-lambda version, I'm not comfortable with lambdas:)
    public static EventHandler<ValueEventArgs<bool>> Link(Publisher publisher, Control subscriber)
    {
         EventHandler<ValueEventArgs<bool>> handler = delegate(object sender, ValueEventArgs<bool> e)
             {
                  subscriber.Enabled = e.Value;
             };
         publisher.EnabledChanged += handler;
         return handler;
    }

    public static void UnLink(Publisher publisher, EventHandler<ValueEventArgs<bool>> handler)
    {
        publisher.EnabledChanged -= handler;
    }

}

请参考在C#中取消订阅匿名方法的示例,了解如何移除委托。


不错,在我的情况下,我甚至可以这样做:subscriber.Disposed += (s, e) => component.ValueChanged -= handler;(哎呀,一个lambda表达式...)。 - Benjol

1

最近我写了一些基于WeakReference的示例代码:

// strongly typed weak reference
public class WeakReference<T> : WeakReference
    where T : class
{
    public WeakReference(T target)
        : base(target)
    { }

    public WeakReference(T target, bool trackResurrection)
        : base(target, trackResurrection)
    { }

    public new T Target
    {
        get { return base.Target as T; }
        set { base.Target = value; }
    }
}

// weak referenced generic event handler
public class WeakEventHandler<TEventArgs> : WeakReference<EventHandler<TEventArgs>>
    where TEventArgs : EventArgs
{
    public WeakEventHandler(EventHandler<TEventArgs> target)
        : base(target)
    { }

    protected void Invoke(object sender, TEventArgs e)
    {
        if (Target != null)
        {
            Target(sender, e);
        }
    }

    public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakEventHandler)
    {
        if (weakEventHandler != null)
        {
            if (weakEventHandler.IsAlive)
            {
                return weakEventHandler.Invoke;
            }
        }

        return null;
    }
}

// weak reference common event handler
public class WeakEventHandler : WeakReference<EventHandler>
{
    public WeakEventHandler(EventHandler target)
        : base(target)
    { }

    protected void Invoke(object sender, EventArgs e)
    {
        if (Target != null)
        {
            Target(sender, e);
        }
    }

    public static implicit operator EventHandler(WeakEventHandler weakEventHandler)
    {
        if (weakEventHandler != null)
        {
            if (weakEventHandler.IsAlive)
            {
                return weakEventHandler.Invoke;
            }
        }

        return null;
    }
}

// observable class, fires events
public class Observable
{
    public Observable() { Console.WriteLine("new Observable()"); }
    ~Observable() { Console.WriteLine("~Observable()"); }

    public event EventHandler OnChange;

    protected virtual void DoOnChange()
    {
        EventHandler handler = OnChange;

        if (handler != null)
        {
            Console.WriteLine("DoOnChange()");
            handler(this, EventArgs.Empty);
        }
    }

    public void Change()
    {
        DoOnChange();
    }
}

// observer, event listener
public class Observer
{
    public Observer() { Console.WriteLine("new Observer()"); }
    ~Observer() { Console.WriteLine("~Observer()"); }

    public void OnChange(object sender, EventArgs e)
    {
        Console.WriteLine("-> Observer.OnChange({0}, {1})", sender, e);
    }
}

// sample usage and test code
public static class Program
{
    static void Main()
    {
        Observable subject = new Observable();
        Observer watcher = new Observer();

        Console.WriteLine("subscribe new WeakEventHandler()\n");
        subject.OnChange += new WeakEventHandler(watcher.OnChange);
        subject.Change();

        Console.WriteLine("\nObserver = null, GC");
        watcher = null;
        GC.Collect(0, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();

        subject.Change();

        if (Debugger.IsAttached)
        {
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }
}

生成以下输出:
new Observable()
new Observer()
subscribe new WeakEventHandler()

DoOnChange()
-> Observer.OnChange(ConsoleApplication4.Observable, System.EventArgs)

Observer = null, GC
~Observer()
DoOnChange()
~Observable()
Press any key to continue . . .

(注意,退订(-=)不起作用)

哇,你是怎么找到这个问题的,它已经迷失在时间的雾中了!我现在手头没有VS,但下周我会试一试。 - Benjol
终于开始尝试这个了 - 显然它对处理程序的释放有些过于热情了。我在可观察的表单中嵌入了观察者控件进行测试。如果我执行GC.Collect,即使控件还在,事件也不再起作用... - Benjol
1
当不再引用事件处理程序时,它将被处理。这有什么过于热情的地方吗? - oɔɯǝɹ

0

Egor的回答基础上,我想尝试构建一个版本,其中我不必预先确定要附加到哪个事件。

我只能让它与通用事件处理程序一起工作:对于“标准”事件处理程序(例如FormClosingEventHandler),这有点棘手,因为您无法具有类型约束where T:delegate(除非您的名字以Pony结尾)。

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 consume)
     Action<S, T> consume)            //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");
            consume(subscriber_strong_ref, e);
        }
        else
        {
            remove(handler);
            handler = null;
        }
    };
    add(handler);
}

(*我在这里尝试使用EventHandler<T> consume,但是调用代码变得很丑陋,因为你必须将s强制转换为Subscriber在consume lambda中。)

调用代码示例,取自上面的示例:

SetAnyGenericHandler(
    h => publisher.EnabledChanged += h, 
    h => publisher.EnabledChanged -= h, 
    subscriber, 
    (Subscriber s, ValueEventArgs<bool> e) => s.Enabled = e.Value);

或者,如果你更喜欢

SetAnyGenericHandler<Subscriber, ValueEventArgs<bool>>(
    h => publisher.EnabledChanged += h, 
    h => publisher.EnabledChanged -= h, 
    subscriber, 
    (s, e) => s.Enabled = e.Value);

如果能够将事件作为一个参数传递进去就好了,但是你无法像访问属性的get/set一样访问事件的add/remove(除非使用反射等不好的方法)。


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