类析构函数问题

我正在创建一个包含StreamWrite的简单类

class Logger { private StreamWriter sw; private DateTime LastTime; public Logger(string filename) { LastTime = DateTime.Now; sw = new StreamWriter(filename); } public void Write(string s) { sw.WriteLine((DateTime.Now-LastTime).Ticks/10000+":"+ s); LastTime = DateTime.Now; } public void Flush() { sw.Flush(); } ~Logger() { sw.Close();//Raises Exception! } } 

但是当我在析构函数中关闭此StreamWriter时,它会引发StreamWriter已被删除的exception?

为什么? 以及如何使其工作,以便在删除Logger类时,StreamWriter在删除之前关闭?

谢谢!

在99.99%的情况下编写自己的析构函数(也就是终结器)是错误的。 需要它们来确保您的类释放一个操作系统资源,该资源不是由.NET框架自动管理的,并且未被您的类用户正确发布。

首先必须在您自己的代码中首先分配操作系统资源。 这总是需要某种P / Invoke。 这是非常需要的,在Microsoft工作的.NET程序员的工作就是照顾它。

他们在StreamWriter的情况下做了。 通过几个层,它是一个文件句柄的包装器,使用CreateFile()创建。 创建句柄的类也是负责编写终结器的类。 Microsoft代码,而不是您的代码。

这样的类总是实现IDisposable,使类的用户有机会在完成资源时释放资源,而不是等待终结器完成工作。 StreamWriter实现了IDisposable。

当然,您的StreamWriter对象是您的类的私有实现细节。 您不知道用户何时完成Logger类,您无法自动调用StreamWriter.Dispose()。 你需要帮助。

通过自己实现IDisposable获得帮助。 您的类的用户现在可以调用Dispose或使用using语句,就像使用任何框架类一样:

  class Logger : IDisposable { private StreamWriter sw; public void Dispose() { sw.Dispose(); // Or sw.Close(), same thing } // etc... } 

嗯,这就是99.9%的情况下应该做的事情。 但不是在这里。 冒着致命混淆的风险:如果你实现了IDisposable,那么你的类的用户也必须有合理的机会来调用它的Dispose()方法。 除了Logger类型类之外,这通常不是什么大问题。 您的用户很可能希望在最后一刻记录某些内容。 例如,她可以从AppDomain.UnhandledException记录未处理的exception。

在这种情况下何时调用Dispose()? 您可以在程序终止时执行此操作。 但是,除了这一点之外,如果在程序退出时发生资源,那么提前释放资源的意义不大。

记录器有特殊要求在所有情况下都能正常关闭。 这要求您将StreamWriter.AutoFlush属性设置为true,以便在写入时立即刷新日志记录输出。 鉴于难以正确调用Dispose(),现在实际上更好的是你没有实现IDisposable并让微软的终结器关闭文件句柄。

Fwiw,框架中有另一个类有同样的问题。 Thread类使用四个操作系统句柄,但不实现IDisposable。 调用Thread.Dispose() 真的很尴尬,所以微软没有实现它。 这会在非常特殊的情况下导致问题,您需要编写一个创建大量线程但从不使用new运算符创建类对象的程序。 它已经完成了。

最后但同样重要的是:如上所述,编写记录器相当棘手。 Log4net是一种流行的解决方案。 NLog是一个更好的捕鼠器,一个感觉和工作的网络,而不是感觉像Java端口。

Destructors(aka Finalizers)不保证以特定顺序运行。 查看文档 :

两个对象的终结器不保证以任何特定顺序运行,即使一个对象引用另一个对象。 也就是说,如果对象A具有对对象B的引用并且两者都具有终结器,则对象B可能已经在对象A的终结器开始时完成。

实现IDisposable

作为一般规则,在实现终结器时,请不要这样做。 它们通常仅在您的类直接使用非托管内存时才需要。 如果您确实实现了Finalizer,它永远不应该引用该类的任何托管成员,因为它们可能不再是有效的引用。

另外需要注意的是,请注意,Finalizer在自己的线程中运行,如果您使用的是具有线程关联性的非托管API,则可能会让您失望。 使用IDisposable和良好,有序清理这些场景更加清晰。