2010-03-27 9 views
6

Ich mache eine einfache Klasse, die eine StreamWrite enthältKlassendestruktor Problem

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! 
    } 
} 

Aber als ich dieses Stream im destructor schließen, wirft es eine Ausnahme, dass der Stream wurde bereits gelöscht?

Warum? Und wie funktioniert es so, dass beim Löschen der Logger-Klasse der StreamWriter vor dem Löschen geschlossen wird?

Danke!

+3

Es gibt ein korrektes Muster zum Implementieren von Einwegobjekten mit Destruktoren. Das ist es nicht. Wie du herausgefunden hast, wenn du das Muster falsch findest, bekommst du die Freude, Fehler im Finalizer-Thread zu debuggen. Informiere dich über das Muster und setze es korrekt um. http://msdn.microsoft.com/en-us/magazine/cc163392.aspx –

Antwort

16

Schreiben Sie Ihren eigenen Destruktor (alias Finalizer) in 99,99% aller Fälle falsch. Sie sind erforderlich, um sicherzustellen, dass Ihre Klasse eine Betriebssystemressource freigibt, die nicht automatisch vom .NET-Framework verwaltet wird und nicht ordnungsgemäß vom Benutzer Ihrer Klasse freigegeben wurde.

Dies beginnt damit, dass Sie zuerst die Betriebssystemressource in Ihrem eigenen Code reservieren müssen. Das erfordert immer eine Art P/Invoke. Dies ist sehr selten erforderlich, war es die Aufgabe der .NET-Programmierer bei Microsoft arbeiten, um das zu kümmern.

Sie taten im Falle von StreamWriter. Durch mehrere Ebenen ist es ein Wrapper um ein Datei-Handle, das mit CreateFile() erstellt wurde. Die Klasse, die das Handle erstellt hat, ist auch diejenige, die für das Schreiben des Finalizers zuständig ist. Microsoft-Code, nicht deins.

Eine solche Klasse implementiert IDisposable immer, sodass der Benutzer der Klasse die Möglichkeit erhält, die Ressource zu veröffentlichen, wenn sie fertig ist, anstatt darauf zu warten, dass der Finalizer die Aufgabe erledigt. StreamWriter implementiert IDisposable.

Ihr StreamWriter-Objekt ist natürlich ein privates Implementierungsdetail Ihrer Klasse. Sie wissen nicht, wann der Benutzer mit Ihrer Logger-Klasse fertig ist. Sie können StreamWriter.Dispose() nicht automatisch aufrufen. Sie benötigen Hilfe.

Holen Sie sich diese Hilfe, indem Sie IDisposable selbst implementieren.Der Benutzer Ihrer Klasse kann Entsorgen jetzt anrufen oder die Anweisung using verwenden, wie sie mit einem der Framework-Klassen würden:

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

Nun, das ist, was man in 99,9% aller Fälle tun soll. Aber nicht hier. Auf die Gefahr hin, Sie fatal zu verwirren: Wenn Sie IDisposable implementieren, muss der Benutzer Ihrer Klasse auch eine angemessene Gelegenheit haben, die Dispose() -Methode aufzurufen. Das ist normalerweise kein großes Problem, außer für eine Logger-Typklasse. Es ist sehr wahrscheinlich, dass Ihr Benutzer sich bis zum letzten Moment anmelden möchte. So kann sie beispielsweise eine nicht behandelte Ausnahme von AppDomain.UnhandledException protokollieren.

Wann sollte Dispose() in diesem Fall aufgerufen werden? Sie könnten es tun, wenn Ihr Programm beendet wird. Aber das ist nicht der Punkt, es hat wenig Sinn, Ressourcen frühzeitig freizugeben, wenn es beim Beenden des Programms passiert.

Ein Logger hat die spezielle Anforderung, in allen Fällen ordnungsgemäß herunterzufahren. Dazu müssen Sie die StreamWriter.AutoFlush-Eigenschaft auf true festlegen, damit die Protokollierungsausgabe sofort nach dem Schreiben gelöscht wird. Angesichts der Schwierigkeit, Dispose() ordnungsgemäß aufzurufen, ist es jetzt besser, IDisposable nicht zu implementieren und Microsoft Finalizer das Dateihandle zu schließen.

Fwiw, es gibt eine andere Klasse im Framework, die das gleiche Problem hat. Die Thread-Klasse verwendet vier Betriebssystemhandles, implementiert jedoch IDisposable nicht. Der Aufruf von Thread.Dispose() ist wirklich peinlich, so Microsoft hat es nicht implementiert. Dies führt zu Problemen, wenn Sie ein Programm schreiben, das viele Threads erstellt, aber nie den neuen Operator zum Erstellen von Klassenobjekten verwendet. Es wurde getan.

Last but not least: Schreiben eines Loggers ist ziemlich schwierig, wie oben erläutert. Log4net ist eine beliebte Lösung. NLog ist eine bessere Mausefalle, eine Bibliothek, die dotnetty fühlt und arbeitet, anstatt sich wie ein Java-Port zu fühlen.

+0

Aus Neugier, tut. NET tatsächlich etwas mit Thread-Handles, bevor ein Thread .Start'ed ist, oder nachdem es beendet ist? Ich verstehe, dass .ManagedThreadId's zugeordnet sind, und so von Finalize wiederhergestellt werden müssen, aber machen die OS-Handles tatsächlich irgendetwas? – supercat

5

Destruktoren (auch Finalisten genannt) können nicht garantiert in einer bestimmten Reihenfolge ausgeführt werden. Siehe the documentation:

Die Finalizer von zwei Objekten werden nicht garantiert in einer bestimmten Reihenfolge ausgeführt, auch wenn ein Objekt auf das andere verweist. Das heißt, wenn Objekt A einen Verweis auf Objekt B hat und beide über Finalizer verfügen, ist Objekt B möglicherweise bereits abgeschlossen, wenn der Finalizer von Objekt A gestartet wird.

Implementieren Sie stattdessen IDisposable.

2

Wenn es um die Implementierung von Finalizern geht, gilt dies in der Regel nicht. Sie werden normalerweise nur benötigt, wenn Ihre Klasse direkt nicht verwalteten Speicher verwendet. Wenn Sie einen Finalizer implementieren, sollte er niemals auf verwaltete Member der Klasse verweisen, da sie möglicherweise keine gültigen Referenzen mehr sind.

Als zusätzlichen Vorbehalt beachten Sie, dass der Finalizer in einem eigenen Thread ausgeführt wird, der Sie stolpern kann, wenn Sie nicht verwaltete APIs mit Thread-Affinität verwenden. Diese Szenarien sind viel sauberer mit IDisposable und nette, ordentliche Bereinigung.