我正在创建一个包含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已被删除的异常?
为什么呢? 以及如何使其工作,以便在删除Logger类时,StreamWriter在删除之前关闭?
谢谢!
答案 0 :(得分:16)
在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中记录未处理的异常。
在这种情况下何时调用Dispose()?您可以在程序终止时执行此操作。但除此之外,如果在程序退出时发生资源,那么提前释放资源的意义不大。
记录器有特殊要求在所有情况下都能正常关闭。这要求您将StreamWriter.AutoFlush属性设置为true,以便在写入时立即刷新日志记录输出。鉴于难以正确调用Dispose(),现在实际上更好的是你没有实现IDisposable并让微软的终结器关闭文件句柄。
Fwiw,框架中有另一个类有同样的问题。 Thread类使用四个操作系统句柄,但不实现IDisposable。调用Thread.Dispose()真的尴尬,所以微软没有实现它。这会导致非常异常情况下出现问题,您需要编写一个创建大量线程但从不使用new运算符创建类对象的程序。它已经完成了。
最后但同样重要的是:如上所述,编写记录器相当棘手。 Log4net是一种流行的解决方案。 NLog是一个更好的捕鼠器,一个感觉和工作的网络,而不是感觉像Java端口。
答案 1 :(得分:5)
Destructors(a.k.a。Finalizers)不保证按特定顺序运行。见the documentation:
两个对象的终结器不保证以任何特定顺序运行,即使一个对象引用另一个对象。也就是说,如果对象A具有对象B的引用并且两者都具有终结器,则当对象A的终结器开始时,对象B可能已经完成。
改为实施IDisposable
。
答案 2 :(得分:2)
作为一般规则,在实现终结器时,请不要这样做。它们通常仅在您的类直接使用非托管内存时才需要。如果你确实实现了Finalizer,它永远不应该引用该类的任何托管成员,因为它们可能不再是有效的引用。
作为一个额外的警告,请注意Finalizer在其自己的线程中运行,如果您使用具有线程关联性的非托管API,则可能会让您失望。使用IDisposable和良好,有序的清理,这些场景更加清晰。