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

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

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

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

43080 次浏览

对你来说,你很好。我最初反过来读你的问题,一个 订户正在走出范围,而不是 出版商。如果事件发布者超出范围,那么向订阅者发送 参考文献(当然不是订阅者本身)顺其自然,没有必要明确地删除它们。

我最初的回答是下面,关于如果您创建一个事件 subscriber并让它在没有取消订阅的情况下超出作用域会发生什么。这并不适用于你的问题,但我将把它留在历史的地方。

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

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

对你来说,一切都很好。它是使事件处理程序的 目标保持活动的事件的 出版对象。因此,如果我有:

publisher.SomeEvent += target.DoSomething;

那么 publisher有一个对 target的引用,而不是反过来。

In your case, the publisher is going to be eligible for garbage collection (assuming there are no other references to it) so the fact that it's got a reference to the event handler targets is irrelevant.

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

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;

(You'd actually want to use a finally block to make sure you don't leak the event handler.) If we didn't unsubscribe, then the BandwidthUI would live at least as long as the transfer service.

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

编辑: 这是对乔纳森·迪金森评论的回应。首先,看看 等于(对象)的文档,它清楚地给出了相等的行为。

其次,这里有一个简短但完整的程序来展示取消订阅的效果:

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

Results:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

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

Further edit:

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

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.5 SP1中; 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