【问题标题】:Garbage collection when using anonymous delegates for event handling使用匿名委托进行事件处理时的垃圾收集
【发布时间】:2010-09-27 03:11:04
【问题描述】:

更新

我已将此处的各种答案合并为new question 上的“确定”答案。

原始问题

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

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's WeakEventHandler 摆弄了一段时间,但它不适用于匿名代表(反正不适合我)。

有没有解决这个问题?我真的很想避免在整个商店复制粘贴样板代码。

(哦,别问我为什么我们一直在创建和销毁控件,这不是我的设计决定......)

(PS:这是一个winforms应用,但是我们已经升级到VS2008和.Net 3.5,我应该考虑使用Weak Event pattern吗?)

(PPS:很好 answer from Rory,但如果有人能想出一个等效于 WeakEventHandler 的方法,这样我就不必记住显式 UnLink/Dispose 了,那就太好了...)

编辑我必须承认我通过“回收”有问题的控件来解决这个问题。然而,解决方法又回来困扰我,因为我使用的“钥匙”显然不是唯一的(呜咽)。我刚刚发现了其他链接here(试过这个 - 似乎有点弱 - 即使目标仍然存在,GC也会清除代表,下面的s,oɔɯǝɹ answer也有同样的问题),@987654327 @(强制您修改发布者,并且实际上不适用于匿名代表)和 here(被 Dustin Campbell 引用为不完整)。

我突然想到,我正在寻找的东西在语义上可能是不可能的——闭包的设计目的是“即使在我离开后也能闲逛”。

我找到了另一种解决方法,所以我会坚持下去,等待voice from the gods

【问题讨论】:

  • @Benjol 哇.. 很高兴看到另一个 lambda 怪胎 :-)
  • 看看 Reactive 框架是否对这个问题有什么要说的会很有趣......

标签: c# event-handling garbage-collection


【解决方案1】:

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

您提到了 Dustin Campbell 的 WeakEventHandler - 它确实不能通过设计使用匿名方法。当我意识到 a) 在 99% 的情况下我需要这样的东西时,我试图将一些东西摆弄在一起,他原来的解决方案会更安全,并且 b) 在我必须这样做的少数情况下(注意:有to,而不是“想要,因为 lambdas 更漂亮和简洁”)如果你有点聪明,它可以让它工作。

您的示例似乎完全是一种一次性的案例,稍微复杂一点就可以得到一个相当简洁的解决方案。


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 库似乎也有点繁重。

【讨论】:

  • 这个问题可能很古老,但我仍然很感兴趣!我已将您的解决方案添加到我的测试应用程序中,并且它似乎有效,因此我很荣幸授予您您的第一个绿色勾号!
  • 谢谢!您还可以查看我的 Dustin Campbell 实现版本。我将其发布为这个问题的答案:stackoverflow.com/questions/1089309/weak-events-in-net/…
  • 哎呀!那个对我来说有点复杂,我得花点时间来剖析它。我正在酝酿这个版本的另一个版本...
【解决方案2】:

Egor's 答案的基础上进一步构建,我想尝试构建一个我不必提前确定要附加到哪个事件的版本。

我只设法使它与通用事件处理程序一起工作:对于“标准”事件处理程序(例如 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&lt;T&gt; consume,但是调用代码变得丑陋,因为您必须在消费 lambda 中将 s 强制转换为订阅者。)

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

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 一样(无需进行令人讨厌的反射操作) ,我认为)。

【讨论】:

    【解决方案3】:

    我最近基于 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,但我下周试试。
    • 终于开始尝试这个 - 显然它有点过于热衷于处理我的处理程序。我使用嵌入在 Observable 形式中的 Observer 控件进行测试。如果我执行 GC.Collect,事件将不再起作用,即使控制仍然存在......
    • 事件处理程序将在不再被引用时被释放。这有什么过分的热情?
    【解决方案4】:

    如果您保留对匿名委托的引用,然后在从表单中删除控件时将其删除,则应该允许控件和匿名委托都被垃圾收集。

    所以是这样的:

    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;
        }
    
    }
    

    有关删除委托的示例,请参阅 Unsubscribe anonymous method in C#

    【讨论】:

    • 很好,就我而言,我什至可以这样做:subscriber.Disposed += (s, e) => component.ValueChanged -= handler; (糟糕,一个 lambda...)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多