类析构函数问题

7

我正在制作一个简单的类,其中包含一个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呢?

谢谢!


3
有一个正确的模式来实现具有析构函数的可释放对象。但这不是它。正如您所发现的,如果模式错误,您将会很高兴地在终结器线程上调试崩溃。请阅读该模式并正确实现。http://msdn.microsoft.com/en-us/magazine/cc163392.aspx - Eric Lippert
3个回答

16
在99.99%的情况下,编写自己的析构函数(也称为 finalizer)都是错误的。只有在你的类使用 .NET 框架无法自动管理且用户未正确释放的操作系统资源时才需要实现它们。
首先,在代码中必须分配操作系统资源。这通常需要 P/Invoke,但非常罕见。这是微软的 .NET 程序员工作的一部分。
例如,StreamWriter 在几层封装后是一个围绕文件句柄的包装器,使用 CreateFile() 创建。创建句柄的类也要负责编写析构函数。这是微软的代码,而不是你的代码。
这样的类总是实现 IDisposable,使得使用类的用户可以在完成操作后释放资源,而不是等待 finalizer 完成工作。StreamWriter 实现了 IDisposable。
当然,你的 Logger 类的 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 中的未处理异常。在这种情况下,你应该设置StreamWriter.AutoFlush属性为true,这样可以在写入时立即刷新日志输出。如果实现Dispose()方法可能很麻烦,那么更好的做法是不实现 IDisposable 接口,而让 Microsoft 的终结器关闭文件句柄。
同样有类似问题的还有 Thread 类,它使用了四个操作系统句柄但没有实现 IDisposable 接口。为了避免尴尬,Microsoft 没有实现Thread.Dispose()方法。这只会在非常少见的情况下造成问题,例如,你需要编写一个程序创建许多线程但从不使用 new 运算符创建类对象。当然这已经被人做过了。
总之,编写日志记录器相当棘手。Log4net 是一种流行的解决方案,NLog 是一个更好的选择,它是一个感觉和工作方式都像 .NET 的库,而不是 Java 移植版。

出于好奇,.net在线程启动之前或终止之后是否会处理线程句柄?我知道分配了.ManagedThreadId,因此必须通过Finalize进行恢复,但是操作系统句柄实际上是否有任何作用? - supercat

5
析构函数(又称为终结器)不保证以特定顺序运行。请参阅文档

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

应该实现IDisposable接口。

2

一般情况下,在实现Finalizers时,不要这么做。只有当您的类直接使用非托管内存时,才需要使用Finalizers。如果您确实要实现Finalizer,请勿引用类中的任何托管成员,因为它们可能不再是有效的引用。

另外需要注意的是,Finalizer在其自己的线程上运行,如果您正在使用具有线程亲和性的非托管API,可能会产生问题。使用IDisposable可以更加优雅、有序地进行清理,这种情况会更加清晰明了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接