Fir*_*oso 32 c# events garbage-collection dispose
我常常发现自己编写的代码如下:
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();
}
Run Code Online (Sandbox Code Playgroud)
目的是清理我们在目标(会话)上注册的所有事件订阅,以便GC可以自由处理会话.我与同事讨论了实现IDisposable的类,但他相信这些类应该像这样执行清理:
/// <summary>
/// Disposes the object
/// </summary>
public void Dispose()
{
SubmitRequested = null; //frees all references to the SubmitRequested Event
}
Run Code Online (Sandbox Code Playgroud)
是否有理由选择一个而不是另一个?有没有更好的方法来解决这个问题?(除了各处的弱参考事件)
我真正希望看到的是一些类似于引发事件的安全调用模式:即安全和可重复.每次我附加到活动时我都记得要做的事情,这样我就可以确保清理起来很容易.
Bri*_*eon 43
说从Session
事件中取消注册处理程序将以某种方式允许Session
GC收集对象是不正确的.这是一个说明参考事件链的图表.
-------------- ------------ ----------------
| | | | | |
|Event Source| ==> | Delegate | ==> | Event Target |
| | | | | |
-------------- ------------ ----------------
Run Code Online (Sandbox Code Playgroud)
所以在你的情况下,事件源是一个Session
对象.但我没有看到你提到哪个类声明了处理程序,所以我们还不知道事件目标是谁.让我们考虑两种可能性.事件目标可以是Session
表示源的同一对象,也可以是完全独立的类.在任何一种情况下,在正常情况下Session
,只要没有其他参考,即使其事件的处理者仍然登记,也将收集.这是因为委托不包含对事件源的引用.它仅包含对事件目标的引用.
请考虑以下代码.
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) { }
}
Run Code Online (Sandbox Code Playgroud)
您将看到"已处置"两次打印到控制台,验证是否收集了两个实例而未取消注册该事件.test2
收集引用的对象的原因是因为它仍然是引用图中的一个孤立实体(一旦test2
设置为null),即使它通过事件返回自身.
现在,事情变得棘手的是,当您希望事件目标的生命周期短于事件源时.在这种情况下,您必须取消注册事件.请考虑以下代码来演示这一点.
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;
}
}
Run Code Online (Sandbox Code Playgroud)
您将看到"处理"从未打印到控制台,表明可能存在内存泄漏.这是一个特别难以处理的问题.实施IDisposable
中Child
,因为没有一种机制保障时,呼叫者会发挥很好,实际上调用不会解决问题Dispose
.
答案
如果您的事件源实现了,IDisposable
那么您还没有真正为自己买过任何新东西.这是因为如果事件源不再是根目录,那么事件目标将不再具有root权限.
如果您的事件目标实现,IDisposable
那么它可以从事件源清除自己,但没有被Dispose
调用的保证.
我并不是说取消注册事件Dispose
是错误的.我的观点是,您确实需要检查如何定义类层次结构,并考虑如果存在内存泄漏问题,最好如何避免内存泄漏问题.
与手动方法相比,实现IDisposable具有两个优点:
using
语句为使用IDisposable提供了特殊的构造。不过,我怀疑这在您的情况下是否有用。为了安全地处置对象,需要将其处置在try / catch中的finally块中。在您似乎要描述的情况下,可能需要在删除对象时(即,在其作用域的末尾:finally块中)由Session来处理此问题,或者要求调用Session的代码进行处理。如果是这样,则会话也必须实现IDisposable,它遵循通用概念。在IDisposable.Dispose方法内部,它循环遍历所有可抛弃的成员并将其处置。
您的最新评论使我重新思考了我的答案,并尝试连接一些点。您想确保Session可被GC使用。如果对委托的引用来自同一类内部,则完全不需要取消订阅。如果它们来自其他班级,则需要取消订阅。查看上面的代码,您似乎可以在使用Session的任何类中编写该代码块,并在过程中的某个时刻对其进行清理。
如果需要释放Session,则有一种更直接的方法是调用类不需要负责正确处理退订过程。只需使用琐碎的反射循环所有事件,并将所有事件设置为null(您可以考虑使用其他方法来达到相同的效果)。
因为您要求“最佳实践”,所以您应该将此方法与结合起来IDisposable
并实现内部的循环IDisposable.Dispose()
。在进入此循环之前,您需要再调用一个事件:Disposing
,如果侦听程序需要自己清理任何内容,则可以使用该事件。使用IDisposable时,请注意其警告,对此简短描述的模式是一个常见的解决方案。