如果我的软件有两个对象实例,其中一个是订阅另一个的事件。我是否需要在他们成为孤儿之前取消订阅他们才能被垃圾收集器清理干净?或者,为什么我应该清除事件关系还有其他原因吗?如果订阅的对象是孤立但订阅者不是,或者反之亦然怎么办?
答案 0 :(得分:23)
是的,你这样做。事件发布者持有对象的引用,并防止它们被垃圾收集。
让我们看一个例子来看看会发生什么。我们有两节课;一个暴露事件,另一个消耗它:
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
所以,总结一下;如果暴露事件的实例超出范围,则无论是否有事件处理程序连接,它都可用于垃圾收集。如果具有事件处理程序的实例连接到另一个实例中的事件,则在分离事件处理程序或附加事件处理程序的实例变为可用于垃圾回收之前,它将不可用于垃圾回收。 / p>
答案 1 :(得分:10)
如果对象公开事件的时间很长,你只需要取消挂钩事件,但是挂钩事件的对象否则将是短暂的(并获得垃圾)收集得相当快。)
在这种情况下,未取消挂钩将导致内存泄漏,因为您的短期对象将无法进行GC操作 - 因为长期对象中的事件保留在委托上,该委托持有对短命对象的引用。由于短代对象仍然被该代理引用,因此无法进行垃圾回收。
静态事件的定义很长 - 它们一直存在直到程序退出。如果你挂钩一个静态事件,你肯定应该在完成后取消它。
如果两个对象即将成为孤儿,则无需取消连接。
答案 2 :(得分:5)
订阅活动会导致对订阅者的强烈引用。这是因为,在封面下,事件是委托,而实例方法的委托是对象引用和实际方法的组合。如果您没有取消订阅,发布者将继续维护引用,并且只要发布者还活着,订阅对象就永远不会真正成为孤儿(和GC)。
反之亦然,即订阅对象没有对发布者的任何引用。