2010-03-27 6 views
6

Je fais une classe simple qui contient un StreamWriteClasse Destructeur Problème

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

Mais quand je ferme cette StreamWriter dans le destructor, il déclenche une exception que la StreamWriter a déjà été supprimé?

Pourquoi? Et comment le faire fonctionner de sorte que lorsque la classe Logger est supprimée, le StreamWriter est fermé avant la suppression?

Merci!

+3

Il existe un modèle correct pour la mise en œuvre d'objets jetables avec des destructeurs. Ce n'est pas ça. Comme vous l'avez découvert, si vous rencontrez le problème, vous aurez la joie de déboguer des plantages sur le thread finalizer. Lisez le motif et mettez-le en place correctement. http://msdn.microsoft.com/fr-fr/magazine/cc163392.aspx –

Répondre

16

L'écriture de votre propre destructeur (également appelé finaliseur) est erronée dans 99,99% des cas. Ils sont nécessaires pour garantir que votre classe libère une ressource de système d'exploitation qui n'est pas automatiquement gérée par le framework .NET et qui n'a pas été correctement publiée par l'utilisateur de votre classe.

Cela commence par devoir allouer la ressource du système d'exploitation en premier dans votre propre code. Cela nécessite toujours une sorte de P/Invoke. C'est très rarement nécessaire, c'était le travail des programmeurs .NET travaillant chez Microsoft pour prendre soin de cela.

Ils l'ont fait dans le cas de StreamWriter. Grâce à plusieurs couches, il s'agit d'un wrapper autour d'un descripteur de fichier, créé avec CreateFile(). La classe qui a créé le handle est également celle responsable de l'écriture du finaliseur. Code Microsoft, pas le vôtre.

Une telle classe implémente toujours IDisposable, ce qui donne à l'utilisateur de la classe la possibilité de libérer la ressource quand elle est faite avec elle, plutôt que d'attendre que le finaliseur fasse le travail. StreamWriter implémente IDisposable.

Bien sûr, votre objet StreamWriter est un détail d'implémentation privée de votre classe. Vous ne savez pas quand l'utilisateur a fini avec votre classe Logger, vous ne pouvez pas appeler StreamWriter.Dispose() automatiquement. Tu as besoin d'aide.

Obtenez cette aide en implémentant IDisposable vous-même.L'utilisateur de votre classe peut maintenant appeler Dispose ou utiliser l'instruction en utilisant, comme elle le ferait avec l'une des classes cadres:

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

Eh bien, c'est ce que vous devez faire dans 99,9% des cas. Mais pas ici. Au risque de vous créer une confusion fatale: si vous implémentez IDisposable, il doit également y avoir une possibilité raisonnable pour l'utilisateur de votre classe d'appeler sa méthode Dispose(). Ce n'est normalement pas un gros problème, sauf pour une classe de type Logger. Il est très probable que votre utilisateur souhaite enregistrer quelque chose au dernier moment possible. Pour qu'elle puisse enregistrer une exception non gérée depuis AppDomain.UnhandledException par exemple. Quand faut-il appeler Dispose() dans ce cas?

Vous pouvez le faire lorsque votre programme se termine. Mais c'est un peu le point, il n'y a pas grand intérêt à libérer des ressources tôt si cela arrive quand le programme sort.

Un enregistreur a l'exigence spéciale de s'arrêter correctement dans tous les cas. Cela nécessite que vous définissiez la propriété StreamWriter.AutoFlush sur true afin que la sortie de journalisation soit vidée dès qu'elle est écrite. Étant donné la difficulté d'appeler Dispose() correctement, il est maintenant préférable de ne pas implémenter IDisposable et de laisser le finaliseur de Microsoft fermer le handle de fichier.

Fwiw, il y a une autre classe dans le cadre qui a le même problème. La classe Thread utilise quatre handles de système d'exploitation mais n'implémente pas IDisposable. Appelant Thread.Dispose() est vraiment maladroit, donc Microsoft ne l'a pas implémenté. Cela provoque des problèmes dans des cas inhabituels, vous auriez besoin d'écrire un programme qui crée beaucoup de threads mais n'utilise jamais le nouvel opérateur pour créer des objets de classe. Cela a été fait. Last but not least: l'écriture d'un enregistreur est assez difficile comme expliqué ci-dessus. Log4net est une solution populaire. NLog est une meilleure souricière, une bibliothèque qui sent et travaille avec dotnetty au lieu de se sentir comme un port Java.

+0

Par curiosité, est-ce que .net fait réellement n'importe quoi avec des handles de threads avant qu'un thread soit .Start'ed, ou après qu'il soit terminé? Je comprends que .ManagedThreadId sont alloués, et doivent donc être récupérés par Finalize, mais les handles du système d'exploitation font-ils réellement quelque chose? – supercat

5

Les destructeurs (c'est-à-dire les finaliseurs) ne sont pas garantis fonctionner dans un ordre particulier. Voir the documentation:

Les finaliseurs de deux objets ne sont pas garantis pour fonctionner dans un ordre spécifique, même si un objet se réfère à l'autre. En d'autres termes, si l'objet A a une référence à l'objet B et que les deux ont des finaliseurs, l'objet B peut déjà être finalisé lorsque le finaliseur de l'objet A démarre.

Mettre en œuvre IDisposable à la place.

2

En règle générale, pour ce qui est de l'implémentation des finaliseurs, ne le faites pas. Ils sont généralement requis uniquement si votre classe utilise directement la mémoire non gérée. Si vous implémentez un Finalizer, il ne doit jamais référencer les membres gérés de la classe, car ils peuvent ne plus être des références valides. En guise d'avertissement supplémentaire, sachez que Finalizer s'exécute dans son propre thread, ce qui peut vous faire trébucher si vous utilisez des API non gérées ayant une affinité de thread. Ces scénarios sont beaucoup plus propres avec IDisposable et agréable, nettoyage ordonné.

Questions connexes