是否有必要在C#中显式删除事件处理程序


120

我有一堂课,提供一些活动。该类是全局声明的,但不能在该全局声明上实例化-可以根据需要在需要它的方法中实例化该类。

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

当方法超出范围时,类的实例也将超出范围。将事件处理程序注册到超出范围的该实例中是否会产生内存占用空间?(我想知道事件处理程序是否使GC不再看到类实例不再被引用。)

Answers:


184

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

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至少将与传输服务一样长。

就我个人而言,我很少碰到这种情况-例如,如果我订阅一个事件,则该事件的目标至少与发布者的寿命一样长-例如,表单将持续到其上的按钮一样长的时间。值得知道这个潜在的问题,但是我认为有些人在不需要时会担心它,因为他们不知道引用的方向。

编辑:这是为了回答乔纳森·迪金森的评论。首先,查看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

2
我同意这一点,但如果可以的话,您能否简要阐述或最好参考“您不想成为订户”这一意思的例子?
Peter McG

@乔恩:非常感谢,这并不常见,但是正如您所说的,我已经看到人们不必要地担心这一点。
Peter McG,

-=不起作用。-=将产生一个新的委托,并且委托不使用目标方法检查相等性,而是对委托执行object.ReferenceEquals()。列表中不存在新的委托:它没有任何作用(并且不会奇怪地抛出错误)。
乔纳森·C·迪金森

2
@乔纳森:不,代表使用目标方法检查是否相等。将在编辑中证明。
乔恩·斯基特

我承认。我对匿名代表感到困惑。
乔纳森·C·迪金森

8

就你而言,你很好。我最初是向后阅读您的问题,即订户超出范围,而不是发布者。如果事件发布者超出范围,则对订阅者的引用(当然,不是订阅者本身!)随之而来,并且不需要显式删除它们。

我的原始答案如下,有关创建事件订阅者并让其超出范围而不取消订阅会发生什么情况。它不适用于您的问题,但我会保留其历史记录。

如果该类仍通过事件处理程序注册,则仍然可以访问。它仍然是一个活物。事件图后面的GC将发现它已连接。是的,您将要显式删除事件处理程序。

仅仅因为该对象超出其原始分配范围并不意味着它是GC的候选对象。只要有实时参考,就可以保持实时。


1
我不相信任何退订必要在这里-在GC看到引用事件发布者,没有它,这是我们很关心这里的出版商。
乔恩·斯基特

@乔恩·斯基特:你是对的。我向后读了这个问题。我已经纠正了我的答案以反映现实。
艾迪
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.