问题描述
我经常发现自己在写这样的代码:
Often I find myself writing code like this:
if (Session != null)
{
Session.KillAllProcesses();
Session.AllUnitsReady -= Session_AllUnitsReady;
Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished;
Session.UnitFailed -= Session_UnitFailed;
Session.SomeUnitsFailed -= Session_SomeUnitsFailed;
Session.UnitCheckedIn -= Session_UnitCheckedIn;
UnattachListeners();
}
目的是清除我们在目标(会话)上注册的所有事件订阅,以便会话可以自由地由 GC 处理.我与一位同事讨论了实现 IDisposable 的类,但他认为这些类应该像这样执行清理:
The purpose being to clean up all event subscriptions that we have registered for on the target (Session) so that Session is free to be disposed by the GC. I had a discussion with a co-worker about classes that implement IDisposable however and it was his belief that those classes should preform cleanup like this:
/// <summary>
/// Disposes the object
/// </summary>
public void Dispose()
{
SubmitRequested = null; //frees all references to the SubmitRequested Event
}
是否有理由偏爱其中一个?有没有更好的方法来解决这个问题?(除了到处都是弱引用事件)
Is there a reason for prefering one over the other? Is there a better way to go about this altogether? (Aside from weak reference events everywhere)
我真正想看到的是类似于引发事件的安全调用模式:即安全且可重复.每次我附加到一个事件时我都记得要做的事情,这样我就可以确保它很容易清理.
What I'd really like to see is somethign akin to the safe invocation pattern for raising events: i.e. safe and repeatable. Something I can remember to do everytime I attach to an event so that I can ensure it will be easy for me to clean up.
推荐答案
说从 Session
事件中注销处理程序会以某种方式允许 Session
对象是不正确的由 GC 收集.这是说明事件参考链的图表.
It is incorrect to say that unregistering the handlers from the Session
events will somehow allow a Session
object to be collected by the GC. Here is a diagram that illustrates the reference chain of events.
-------------- ------------ ----------------
| | | | | |
|Event Source| ==> | Delegate | ==> | Event Target |
| | | | | |
-------------- ------------ ----------------
所以在你的情况下,事件源是一个 Session
对象.但是我没有看到你提到哪个类声明了处理程序,所以我们还不知道事件目标是谁.让我们考虑两种可能性.事件目标可以是表示源的同一个 Session
对象,也可以是一个完全独立的类.在任何一种情况下,在正常情况下,只要没有其他引用,即使对其事件的处理程序保持注册,也将收集 Session
.这是因为委托不包含对事件源的引用.它只包含对事件目标的引用.
So in your case the event source is a Session
object. But I do not see that you mentioned which class declared the handlers so we do not yet known who the event target is. Lets consider two possibilities. The event target could be the same Session
object that represents the source or it could be an entirely separate class. In either case and under normal circumstances the Session
will be collected as long as there is not another reference to even if the handlers to its events remain registered. That is because the delegate does not contain a reference back to the event source. It only contains a reference to the event target.
考虑以下代码.
public static void Main()
{
var test1 = new Source();
test1.Event += (sender, args) => { Console.WriteLine("Hello World"); };
test1 = null;
GC.Collect();
GC.WaitForPendingFinalizers();
var test2 = new Source();
test2.Event += test2.Handler;
test2 = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
public class Source()
{
public event EventHandler Event;
~Source() { Console.WriteLine("disposed"); }
public void Handler(object sender, EventArgs args) { }
}
您将看到disposed"在控制台上打印了两次,以验证两个实例是否已在未注销事件的情况下收集.test2
引用的对象被收集的原因是因为它在引用图中仍然是一个孤立的实体(一旦 test2
被设置为 null),即使它有一个引用通过事件回到自己.
You will see that "disposed" is printed twice to the console verifying that both instances were collected without unregistering the event. The reason the object referenced by test2
gets collected is because it remains an isolated entity in the reference graph (once test2
is set to null that is) even though it has a reference back to itself though the event.
现在,当您希望事件目标的生命周期短于事件源时,事情变得棘手.在这种情况下,您必须取消注册事件.考虑以下演示这一点的代码.
Now, where things get tricky is when you want to have the event target have a lifetime that is shorter than the event source. In that case you have to unregister the events. Consider the following code that demonstrates this.
public static void Main()
{
var parent = new Parent();
parent.CreateChild();
parent.DestroyChild();
GC.Collect();
GC.WaitForPendingFinalizers();
}
public class Child
{
public Child(Parent parent)
{
parent.Event += this.Handler;
}
private void Handler(object sender, EventArgs args) { }
~Child() { Console.WriteLine("disposed"); }
}
public class Parent
{
public event EventHandler Event;
private Child m_Child;
public void CreateChild()
{
m_Child = new Child(this);
}
public void DestroyChild()
{
m_Child = null;
}
}
您会看到disposed"永远不会打印到控制台,这表明可能存在内存泄漏.这是一个特别难处理的问题.在 Child
中实现 IDisposable
并不能解决问题,因为无法保证调用者会玩得很好并实际调用 Dispose
.
You will see that "disposed" is never printed to the console demonstrating a possible memory leak. This is a particularly difficult problem to deal with. Implementing IDisposable
in Child
will not solve the problem because there is no guarentee that callers will play nicely and actually call Dispose
.
答案
如果您的事件源实现了 IDisposable
,那么您还没有真正为自己购买任何新东西.那是因为如果事件源不再根,那么事件目标也将不再根.
If your event source implements IDisposable
then you have not really bought yourself anything new. That is because if the event source is no longer rooted than the event target will no longer be rooted as well.
如果您的事件目标实现了 IDisposable
,那么它可以从事件源中清除自身,但不保证 Dispose
会被调用.
If your event target implements IDisposable
then it could clear itself from the event source but there is no guarentee that Dispose
will get called.
我并不是说从 Dispose
取消注册事件是错误的.我的观点是,您确实需要检查您的类层次结构是如何定义的,并考虑如何最好地避免内存泄漏问题(如果存在的话).
I am not saying that unregistering events from Dispose
is wrong. My point is that you really need to examine how your class hierarchy is defined and consider how you might best avoid the memory leak problem if one even exists.
这篇关于清理事件处理程序引用的最佳实践是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!