【问题标题】:Is it necessary to explicitly remove event handlers in C#是否有必要在 C# 中显式删除事件处理程序
【发布时间】:2009-02-03 05:28:23
【问题描述】:

我有一个提供一些活动的课程。该类是全局声明的,但不会在该全局声明上实例化——它是在需要它的方法中根据需要实例化的。

每次在方法中需要该类时,都会对其进行实例化并注册事件处理程序。是否有必要在方法超出范围之前显式删除事件处理程序?

当方法超出范围时,类的实例也会超出范围。离开注册到超出范围的实例的事件处理程序是否会影响内存占用? (我想知道事件处理程序是否阻止 GC 将类实例视为不再被引用。)

【问题讨论】:

    标签: c# garbage-collection event-handling


    【解决方案1】:

    在你的情况下,一切都很好。它是发布事件的对象,它使事件处理程序的目标保持活动状态。所以如果我有:

    publisher.SomeEvent += target.DoSomething;
    

    那么publisher 引用了target,但反之则不然。

    在您的情况下,发布者将有资格进行垃圾收集(假设没有其他对它的引用),因此它获得对事件处理程序目标的引用这一事实是无关紧要的。

    棘手的情况是发布者长期存在但订阅者不想成为 - 在 情况下,您需要取消订阅处理程序。例如,假设您有一些数据传输服务,可以让您订阅有关带宽更改的异步通知,并且传输服务对象是长期存在的。如果我们这样做:

    BandwidthUI ui = new BandwidthUI();
    transferService.BandwidthChanged += ui.HandleBandwidthChange;
    // Suppose this blocks until the transfer is complete
    transferService.Transfer(source, destination);
    // We now have to unsusbcribe from the event
    transferService.BandwidthChanged -= ui.HandleBandwidthChange;
    

    (您实际上希望使用 finally 块来确保您不会泄漏事件处理程序。)如果我们不取消订阅,那么BandwidthUI 将至少与传输服务一样长。

    就我个人而言,我很少遇到这种情况——通常,如果我订阅一个事件,该事件的目标至少与发布者一样长——例如,一个表单的持续时间与它上面的按钮一样长。这个潜在问题值得了解,但我认为有些人在不需要时会担心它,因为他们不知道参考文献的走向。

    编辑:这是为了回答 Jonathan Dickinson 的评论。首先,查看Delegate.Equals(object) 的文档,它清楚地给出了平等行为。

    其次,这是一个简短但完整的程序来显示退订工作:

    using System;
    
    public class Publisher
    {
        public event EventHandler Foo;
    
        public void RaiseFoo()
        {
            Console.WriteLine("Raising Foo");
            EventHandler handler = Foo;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
            else
            {
                Console.WriteLine("No handlers");
            }
        }
    }
    
    public class Subscriber
    {
        public void FooHandler(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber.FooHandler()");
        }
    }
    
    public class Test
    {
        static void Main()
        {
             Publisher publisher = new Publisher();
             Subscriber subscriber = new Subscriber();
             publisher.Foo += subscriber.FooHandler;
             publisher.RaiseFoo();
             publisher.Foo -= subscriber.FooHandler;
             publisher.RaiseFoo();
        }
    }
    

    结果:

    Raising Foo
    Subscriber.FooHandler()
    Raising Foo
    No handlers
    

    (在 Mono 和 .NET 3.5SP1 上测试。)

    进一步编辑:

    这是为了证明在仍有对订阅者的引用时可以收集事件发布者。

    using System;
    
    public class Publisher
    {
        ~Publisher()
        {
            Console.WriteLine("~Publisher");
            Console.WriteLine("Foo==null ? {0}", Foo == null);
        }
    
        public event EventHandler Foo;
    }
    
    public class Subscriber
    {
        ~Subscriber()
        {
            Console.WriteLine("~Subscriber");
        }
    
        public void FooHandler(object sender, EventArgs e) {}
    }
    
    public class Test
    {
        static void Main()
        {
             Publisher publisher = new Publisher();
             Subscriber subscriber = new Subscriber();
             publisher.Foo += subscriber.FooHandler;
    
             Console.WriteLine("No more refs to publisher, "
                 + "but subscriber is alive");
             GC.Collect();
             GC.WaitForPendingFinalizers();         
    
             Console.WriteLine("End of Main method. Subscriber is about to "
                 + "become eligible for collection");
             GC.KeepAlive(subscriber);
        }
    }
    

    结果(在 .NET 3.5SP1 中;Mono 在这里的行为似乎有点奇怪。一段时间后会调查):

    No more refs to publisher, but subscriber is alive
    ~Publisher
    Foo==null ? False
    End of Main method. Subscriber is about to become eligible for collection
    ~Subscriber
    

    【讨论】:

    • 我同意这一点,但如果可能的话,您能否简要说明或最好参考一个示例来说明您所说的“但订阅者不想成为”?
    • -= 不起作用。 -= 将产生一个新的委托,并且委托不使用目标方法检查相等性,他们在委托上执行 object.ReferenceEquals()。列表中不存在新的委托:它没有任何效果(并且没有抛出足够奇怪的错误)。
    • @Jonathan:不,代表使用目标方法检查相等性。将在编辑中证明。
    • 我承认。我对匿名代表感到困惑。
    • @JonSkeet:+1 非常感谢您提供非常好的解释。您说过“(您实际上希望使用 finally 块来确保您不会泄漏事件处理程序。)如果我们不取消订阅,那么 BandwidthUI 将至少与传输服务一样长。”这是否意味着此类事件取消注册应该发生在 Dispose 方法中的 if(disposing) 子句之外?我在这里问过类似的问题 (stackoverflow.com/questions/7642812/…)
    【解决方案2】:

    在你的情况下,你很好。我最初是向后阅读您的问题,即 subscriber 超出了范围,而不是 publisher。如果事件发布者超出范围,那么对订阅者(当然不是订阅者本身!)的引用也会随之而来,无需显式删除它们。

    我的原始答案如下,关于如果您创建一个事件 subscriber 并让它超出范围而不取消订阅会发生什么。它不适用于您的问题,但我会将其留作历史。

    如果类仍然通过事件处理程序注册,那么它仍然是可访问的。它仍然是一个活的物体。遵循事件图的 GC 会发现它已连接。是的,您需要明确删除事件处理程序。

    仅仅因为对象超出其原始分配的范围并不意味着它是 GC 的候选对象。只要实时引用仍然存在,它就是实时的。

    【讨论】:

    • 我认为这里没有必要取消订阅 - GC 看到的是 来自 事件发布者的引用,而不是它的引用,而我们在这里关注的是发布者。
    • @Jon Skeet:你是对的。我倒着读了这个问题。我已经更正了我的答案以反映现实。
    猜你喜欢
    • 2010-11-14
    • 2011-09-26
    • 1970-01-01
    • 2010-11-21
    • 1970-01-01
    • 2012-01-22
    • 2012-09-04
    • 2011-07-05
    相关资源
    最近更新 更多