我是否需要在孤立之前从对象中删除事件订阅?

如果我的软件有两个对象实例,其中一个订阅了另一个的事件。 我是否需要在他们成为孤儿之前取消订阅他们才能被垃圾收集器清理干净? 或者,为什么我应该清除事件关系还有其他原因吗? 如果订阅的对象是孤立但订阅者不是,或反过来怎么办?

是的你是。 事件发布者持有对对象的引用,并且会阻止它们被垃圾回收。

让我们看一个例子来看看会发生什么。 我们有两节课; 一个暴露事件,另一个消耗它:

class ClassA { public event EventHandler Test; ~ClassA() { Console.WriteLine("A being collected"); } } class ClassB { public ClassB(ClassA instance) { instance.Test += new EventHandler(instance_Test); } ~ClassB() { Console.WriteLine("B being collected"); } void instance_Test(object sender, EventArgs e) { // this space is intentionally left blank } } 

注意ClassB如何不存储对ClassA实例的引用; 它只是挂钩一个事件处理程序。

现在,让我们看看如何收集对象。 场景1:

 ClassB temp = new ClassB(new ClassA()); Console.WriteLine("Collect 1"); GC.Collect(); Console.ReadKey(); temp = null; Console.WriteLine("Collect 2"); GC.Collect(); Console.ReadKey(); 

我们创建一个ClassB实例并通过temp变量保存对它的引用。 它传递了一个ClassA的新实例,我们不在任何地方存储对它的引用,因此它在ClassB构造函数完成后立即超出范围。 当ClassA超出范围时,我们将垃圾收集器运行一次,而当ClassB超出范围时,我们运行一次。 输出:

 Collect 1 A being collected Collect 2 B being collected 

场景2:

 ClassA temp = new ClassA(); ClassB temp2 = new ClassB(temp); temp2 = null; Console.WriteLine("Collect 1"); GC.Collect(); Console.ReadKey(); temp = null; Console.WriteLine("Collect 2"); GC.Collect(); Console.ReadKey(); 

创建一个新的ClassA实例,并将对它的引用存储在temp变量中。 然后创建一个新的ClassB实例,将TempA中的ClassA实例传递给它,并在temp2中存储对它的引用。 然后我们将temp2设置为null,使ClassB实例超出范围。 和以前一样,我们在每个实例超出范围后运行垃圾收集器。 输出:

 Collect 1 Collect 2 B being collected A being collected 

所以,总结一下; 如果暴露事件的实例超出范围,则无论是否有事件处理程序连接,它都可用于垃圾收集。 如果具有事件处理程序的实例连接到另一个实例中的事件,则在分离事件处理程序或附加事件处理程序的实例变为可用于垃圾回收之前,它将不可用于垃圾回收。

如果暴露事件的对象是长期存在的,那么您只需要取消挂钩事件,但是挂起事件的对象否则将是短暂的(并且相当快地收集垃圾)。

在这种情况下,未取消挂钩将导致内存泄漏,因为您的短期对象将无法进行GC操作 – 因为长期对象中的事件保留在委托上,该委托持有对短暂的对象。 由于短代对象仍然被该代理引用,因此无法进行垃圾回收。

根据定义,静态事件是长寿命的 – 它们一直存在直到程序退出。 如果你挂钩一个静态事件,你肯定应该在你完成后取消它。

如果两个对象即将成为孤儿,则不需要取消连接。

订阅事件会导致对订户的强烈引用。 这是因为,在封面下,事件是委托,而实例方法的委托是对象引用和实际方法的组合。 如果您不取消订阅,则发布者将继续维护引用,并且只要发布者处于活动状态,订阅对象就永远不会真正成为孤儿(和GC)。

反之亦然,即订阅对象没有对发布者的任何引用。