如何在最终确定静态变量之前得到通知

我什么时候可以清理存储在C#中的静态变量中的对象?

我有一个懒惰的初始化静态变量:

public class Sqm { private static Lazy _default = new Lazy(); public static Sqm Default { get { return _default.Value; } } } 

注意 :我刚刚将Foo改为static类。 如果Foo是静态的,它不会以任何方式改变问题。 但有些人确信,如果不先构建一个Foo实例,就不可能构造一个Sqm实例。 即使我确实创建了一个Foo对象; 即使我创建了100个,它也无法解决问题(何时“清理”静态成员)。

样品用法

 Foo.Default.TimerStart("SaveQuestion"); //...snip... Foo.Default.TimerStop("SaveQuestion"); 

现在,我的Sqm类实现了一个方法,当不再需要该对象时必须调用该方法,并且需要自行清理(将状态保存到文件系统,释放锁等)。 必须在垃圾收集器运行之前调用此方法(即在调用对象的终结器之前):

 public class Sqm { var values = new List(); Boolean shutdown = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Shutdown() { if (!alreadyShutdown) { Cleanup(values); alreadyShutdown = true; } } } 

何时,何地,我可以调用我的Shutdown()方法?

注意 :我不希望使用 Sqm类的开发人员担心调用Shutdown 。 那不是他的工作。 在其他语言环境中,他不必这样做。

Lazy类似乎没有在它懒惰拥有的Value上调用Dispose 。 所以我无法挂钩IDisposable模式 – 并将其用作调用Shutdown的时间。 我需要自己打电话给Shutdown

但当?

它是一个static变量,它在应用程序/域/ appdomain / apartment的生命周期中存在一次。

是的,终结者是错误的时间

有些人确实理解,而有些人却不明白,在finalizer器中尝试上传我的数据是错误的

 ///WRONG: Don't do this! ~Sqm { Shutdown(_values); //<-- BAD! _values might already have been finalized by the GC! } 

为什么这是错的? 因为values可能不再存在了。 您无法控制以什么顺序最终确定的对象。 完全有可能在包含Sqm之前确定values

处置怎么样?

IDisposable接口和Dispose()方法是一种约定 。 如果我的对象实现了一个将被调用的Dispose()方法,那就没有任何规定。 事实上,我可以继续实施它:

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(values); alreadyDiposed = true; } } } 

对于实际阅读问题的人,您可能会注意到我实际上没有改变任何东西。 我唯一做的就是将方法的名称从Shutdown更改为Dispose 。 Dispose模式只是一种约定。 我还有问题:我什么时候可以拨打Dispose

那么你应该从你的终结者那里打电话给你

从我的终结器调用Dispose与从终结器调用Shutdown一样不正确(它们完全相同):

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(_values); // <--BUG: _values might already have been finalized by the GC! alreadyDiposed = true; } } ~Sqm { Dispose(); } } 

因为, values可能再也不存在了。 为了完整起见,我们可以返回完整的原始正确代码:

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } ~Sqm { Dispose(false); //false ==> it is not safe to access values } } 

我已经完整了。 我有一个对象,我需要在应用程序域关闭之前“清理” 。 当我可以调用Cleanup时,需要通知我的对象内部的某些内容。

让开发人员调用它

没有。

我正在将现有概念从另一种语言迁移到C#中。 如果开发人员碰巧使用全局单例实例:

 Foo.Sqm.TimerStart(); 

那么Sqm类是懒惰初始化的。 在(本机)应用程序中,保持对对象的引用。 在(本机)应用程序关闭期间,保存接口指针的变量设置为null ,并且调用单例对象的destructor ,并且它可以自我清理。

任何人都不应该打电话。 不Cleanup ,不Shutdown ,不Dispose 。 关闭应该由基础设施自动发生。

什么是C#相当于我看到自己走开,清理自己

如果你让垃圾收集器收集对象这一事实很复杂:现在为时已晚。 我想要保留的内部状态对象可能已经完成。

如果来自ASP.net会很容易

如果我可以保证我的类是从ASP.net使用的,我可以要求HostingEnvironment在域关闭之前通过注册我的对象来通知它:

 System.Web.Hosting.HostingEnvironment.RegisterObject(this); 

并实现Stop方法:

 public class Sqm : IDisposable, IRegisteredObject { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } Sqm { //Register ourself with the ASP.net hosting environment, //so we can be notified with the application is shutting down HostingEnvironment.RegisterObject(this); //asp.net will call Stop() when it's time to cleanup } ~Sqm { Dispose(false); //false ==> it is not safe to access values } // IRegisteredObject protected void Stop(Boolean immediate) { if (immediate) { //i took too long to shut down; the rug is being pulled out from under me. //i had my chance. Oh well. return; } Cleanup(); //or Dispose(), both good } } 

除了我的课程不知道我是从ASP.netWinFormsWPF ,控制台应用程序,还是shell扩展调用。

编辑 :人们似乎对IDisposable模式的存在感到困惑。 删除了对Dispose引用以消除混淆。

编辑2 :在他们回答问题之前,人们似乎要求完整,详细的示例代码。 我个人认为这个问题已经包含了太多的示例代码,因为它不能帮助提出问题。

现在我已经添加太多的代码,问题已经丢失了。 在问题合理之前,人们拒绝回答问题。 既然这是合理的,没有人会读它。

这就像诊断

它就像System.Diagnostics.Trace类。 人们在他们想要的时候叫它:

 Trace.WriteLine("Column sort: {0} ms", sortTimeInMs); 

而且再也不用想了。

然后绝望陷入困境

我甚至非常绝望,我考虑将我的对象隐藏在COM IUnknown接口后面,这是引用计数

 public class Sqm : IUnknown { IUnknown _default = new Lazy(); } 

然后希望我可以欺骗 CLR减少我的界面上的引用计数。 当我的引用计数变为零时,我知道一切都在关闭。

其缺点是我无法使其发挥作用。

这里有两个问题:

  • 您坚持认为List可能已经完成。 List没有终结器,它也不会被垃圾收集(因为你有一个引用它)。 (这些是不同的操作。)您的SQL终结器仍将看到有效数据。 实际上,终结器可能没问题 – 虽然当终结器运行时,你需要的其他资源可能已经消失了 – 终结器甚至可能不会被调用。 所以我认为这同时比你预期的更可行 – 而且总的来说更糟糕。

  • 你坚持认为你不想把它放在开发者的控制之下,无论是否使用IDisposable 。 这只是与.NET提供的内容作斗争。 垃圾收集器用于存储器资源; 任何需要确定性清理(包括刷新等)的非内存资源都应该明确清理。 您可以使用终结器作为最后的“尽力而为”清理,但不应该以您尝试使用它的方式使用它。

可以使用一些方法来尝试解决此问题,例如使用“canary”对象并引用“真实”对象:保留对您在其他地方感兴趣的对象的强引用,并使用终结器只是在金丝雀对象中,所以唯一要确定的是金丝雀对象 – 然后触发适当的刷新并删除最后一个强引用,使真实对象符合GC的条件 – 但它仍然是一个根本不好的主意,并且混合中的静态变量变得更糟。

同样,您可以使用AppDomain.DomainUnload事件 – 但同样,我不会。 当域被卸载时,我会担心其余对象的状态 – 并且不会调用默认域。

基本上,我认为你应该改变你的设计。 我们并不真正了解您尝试设计的API的背景,但您现在的方式将无法正常工作。 我会尽量避免使用静态变量 – 至少对于任何在时间方面很重要的事情都是如此。 幕后可能仍然有一个对象用于协调,但是在你的API中暴露出来对我来说是个错误。 无论你如何抗议其他语言和其他平台,如果你在.NET工作,你需要接受它就是它。 从长远来看,与系统作斗争并不会对你有所帮助。

您越早得出结论,您需要更改API设计,您需要花费更多时间来考虑新API应该是什么样子。

AppDomain上有一个ProcessExit事件你可以尝试挂钩,虽然我不太了解它,它的默认时间限制为2秒。

这样的东西(如果它适合你);

 class SQM { static Lazy _Instance = new Lazy( CreateInstance ); private static SQM CreateInstance() { AppDomain.CurrentDomain.ProcessExit += new EventHandler( Cleanup ); return new SQM(); } private static Cleanup() { ... } } 

除了Ken的答案之外,还有“如何丢弃我的物体?”的答案。 是的,你不能。

您正在寻找的概念是静态解构函数,或者在释放静态方法时运行的解构函数。 这在托管代码中不存在 ,并且在大多数(所有?)情况下不应该是必需的。 当可执行文件结束时,您很可能会查看正在卸载的静态方法,操作系统将在此时清除所有内容。

如果您绝对需要释放资源,并且必须在所有活动实例之间共享此对象,则可以创建引用计数器并在确定已释放所有引用时处置该对象。 我会首先考虑这是否是正确的方法。 新实例需要validation您的对象是否为null ,如果是,则再次实例化它。

您不应该调用Dispose 。 如果实现IDisposable的类仅使用托管资源,那么这些资源将在程序完成时自然发布。 如果类使用未受管理的资源,那么该类应该扩展CriticalFinalizerObject并在其终结器(以及其Dispose方法)中释放这些资源。

换句话说,正确使用IDisposable接口不需要调用Dispose 。 可以调用它来在程序中的特定点释放托管或非托管资源,但是由于调用它而发生的泄漏应该被视为错误。

编辑

什么是C#相当于我看到自己走开,清理自己

在回答您编辑过的问题时,我认为您正在寻找终结者:

 class Foo { ~Foo() { // Finalizer code. Called when garbage collected, maybe... } } 

但请注意,不能保证调用此方法。 如果你绝对需要它,你应该扩展System.Runtime.ConstrainedExecution.CriticalFinalizerObject

不过,我可能仍会对你的问题感到困惑。 终结器绝对不是 “将我的内部值保存到文件中”的地方。

AppDomain域卸载事件似乎非常适合您正在寻找的内容。 由于静态变量一直存在,直到AppDomain被卸载,这应该在变量被销毁之前给你一个钩子。

你花了这么多时间来对抗语言,为什么不重新设计以便问题不存在?

例如,如果您需要保存变量的状态,而不是在它被销毁之前尝试捕获它,请在每次修改时保存它,并覆盖以前的状态。

如果有四种不同的方式,我曾四次问过这个问题。 每个人的措辞略有不同; 试图从不同的方向解决问题。 最后是MA Hanin指出了我解决这个问题的问题。

问题是没有一种方法可以知道域何时关闭。 你能做的最好的事情就是尝试捕捉100%(四舍五入到最接近的百分比)时间的各种事件。

如果代码在默认的某个域中,则使用DomainUnload事件。 不幸的是, 默认的 AppDomain不会引发DomainUnload事件。 那么我们捕获ProcessExit

 class InternalSqm { //constructor public InternalSqm () { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); } ... } 

这已经在“网站”“WinForms”应用程序中进行了测试。

代码越完整,显示了IDisposable的实现:

 class InternalSqm : IDisposable { private Boolean _disposed = false; //constructor public InternalSqm() { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); } 

  ///  /// Finalizer (Finalizer uses the C++ destructor syntax) ///  ~InternalSqm() { Dispose(false); //False: it's not safe to access managed members } public void Dispose() { this.Dispose(true); //True; it is safe to access managed members GC.SuppressFinalize(this); //Garbage collector doesn't need to bother to call finalize later } protected virtual void Dispose(Boolean safeToAccessManagedResources) { if (_disposed) return; //be resilient to double calls to Dispose try { if (safeToAccessManagedResources) { // Free other state (managed objects). this.CloseSession(); //save internal stuff to persistent storage } // Free your own state (unmanaged objects). // Set large fields to null. Etc. } finally { _disposed = true; } } } 

样品用法

从进行图像处理的库中:

 public static class GraphicsLibrary { public Image RotateImage(Image image, Double angleInDegrees) { Sqm.TimerStart("GraphicaLibrary.RotateImage"); ... Sqm.TimerStop("GraphicaLibrary.RotateImage"); } } 

从可以执行查询的帮助程序类

 public static class DataHelper { public IDataReader ExecuteQuery(IDbConnection conn, String sql) { Sqm.TimerStart("DataHelper_ExecuteQuery"); ... Sqm.TimerStop("DataHelper_ExecuteQuery"); } } 

对于WinForms主题绘图

 public static class ThemeLib { public void DrawButton(Graphics g, Rectangle r, String text) { Sqm.AddToAverage("ThemeLib/DrawButton/TextLength", text.Length); } } 

在网站中:

 private void GetUser(HttpSessionState session) { LoginUser user = (LoginUser)session["currentUser"]; if (user != null) Sqm.Increment("GetUser_UserAlreadyFoundInSession", 1); ... } 

在扩展方法中

 ///  /// Convert the guid to a quoted string ///  /// A Guid to convert to a quoted string ///  public static string ToQuotedStr(this Guid source) { String s = "'" + source.ToString("B") + "'"; //B=braces format "{6CC82DE0-F45D-4ED1-8FAB-5C23DE0FF64C}" //Record how often we dealt with each type of UUID Sqm.Increment("String.ToQuotedStr_UUIDType_"+s[16], 1); return s; } 

注意 :任何代码都将发布到公共域中。 无需归属。

它们将持续AppDomain的持续时间。 对方法可见对静态变量所做的更改。

MSDN:

如果使用Static关键字声明局部变量,则其生命周期长于声明它的过程的执行时间。 如果过程在模块内部,只要应用程序继续运行,静态变量就会存在。