Quelle est la différence entre l'utilisation d'IDisposable et d'un destructeur en C #?

101

Quand est-ce que j'implémenterai IDispose sur une classe par opposition à un destructeur? J'ai lu cet article , mais je manque toujours le point.

Mon hypothèse est que si j'implémente IDispose sur un objet, je peux explicitement le «détruire» plutôt que d'attendre que le garbage collector le fasse. Est-ce correct?

Cela signifie-t-il que je devrais toujours appeler explicitement Dispose sur un objet? Quels sont quelques exemples courants de cela?

Jordan Parmer
la source
5
En effet, vous devez appeler Dispose sur chaque objet jetable. Vous pouvez le faire facilement en utilisant la usingconstruction.
Luc Touraille
Ah, ça a du sens. Je m'étais toujours demandé pourquoi l'instruction «using» était utilisée pour les flux de fichiers. Je sais que cela avait quelque chose à voir avec la portée de l'objet, mais je ne l'ai pas mis en contexte avec l'interface IDisposable.
Jordan Parmer
5
Un point important à retenir est qu'un finaliseur ne doit jamais accéder aux membres gérés d'une classe, car ces membres peuvent ne plus être des références valides.
Dan Bryant

Réponses:

126

Un finaliseur (alias destructeur) fait partie du ramasse-miettes (GC) - il est indéterminé quand (ou même si) cela se produit, car GC se produit principalement en raison de la pression de la mémoire (c'est-à-dire qu'il faut plus d'espace). Les finaliseurs ne sont généralement utilisés que pour nettoyer les ressources non gérées , car les ressources gérées auront leur propre collecte / élimination.

Par conséquent, il IDisposableest utilisé pour nettoyer les objets de manière déterministe , c'est-à-dire maintenant. Il ne collecte pas la mémoire de l'objet (qui appartient toujours à GC) - mais est utilisé par exemple pour fermer des fichiers, des connexions à la base de données, etc.

Il y a beaucoup de sujets précédents à ce sujet:

Enfin, notez qu'il n'est pas rare qu'un IDisposableobjet ait également un finaliseur; dans ce cas, il Dispose()appelle généralement GC.SuppressFinalize(this), ce qui signifie que GC ne lance pas le finaliseur - il jette simplement la mémoire (beaucoup moins cher). Le finaliseur s'exécute toujours si vous oubliez Dispose()l'objet.

Marc Gravell
la source
Merci! Cela est parfaitement logique. J'apprécie beaucoup la bonne réponse.
Jordan Parmer
27
Une chose supplémentaire à dire. N'ajoutez pas de finaliseur à votre classe sauf si vous en avez vraiment, vraiment besoin. Si vous ajoutez un finaliseur (destructeur), le GC doit l'appeler (même un finaliseur vide) et pour l'appeler, l'objet survivra toujours à un ramasse-miettes de génération 1. Cela gênera et ralentira le GC. C'est ce que Marc dit d'appeler SuppressFinalize dans le code ci-dessus
Kevin Jones
1
Finaliser consiste donc à libérer des ressources non gérées. Mais Dispose pourrait être utilisé pour libérer des ressources gérées et non gérées?
Dark_Knight
2
@Dark oui; parce que 6 niveaux plus bas dans la chaîne de gestion pourraient être non gérés qui nécessitent un nettoyage rapide
Marc Gravell
1
@KevinJones Les objets avec un finaliseur sont garantis pour survivre à la génération 0, pas 1, non? J'ai lu cela dans un livre intitulé .NET Performance.
David Klempfner
25

Le rôle de la Finalize()méthode est de garantir qu'un objet .NET peut nettoyer les ressources non gérées lors du nettoyage de la mémoire . Cependant, les objets tels que les connexions à la base de données ou les gestionnaires de fichiers doivent être libérés dès que possible, au lieu de s'appuyer sur le garbage collection. Pour cela, vous devez implémenter l' IDisposableinterface et libérer vos ressources dans la Dispose()méthode.

Igal Tabachnik
la source
9

Il y a une très bonne description sur MSDN :

L'utilisation principale de cette interface est de libérer des ressources non gérées . Le garbage collector libère automatiquement la mémoire allouée à un objet géré lorsque cet objet n'est plus utilisé. Cependant, il n'est pas possible de prédire le moment de la récupération de place . En outre, le garbage collector n'a aucune connaissance des ressources non gérées telles que les descripteurs de fenêtre ou les fichiers et flux ouverts .

Utilisez la méthode Dispose de cette interface pour libérer explicitement les ressources non managées conjointement avec le garbage collector. Le consommateur d'un objet peut appeler cette méthode lorsque l'objet n'est plus nécessaire.

abatishchev
la source
1
Une faiblesse majeure de cette description est que MS donne des exemples de ressources non gérées, mais d'après ce que j'ai vu, le terme n'a jamais été défini. Étant donné que les objets gérés ne sont généralement utilisables que dans du code managé, on pourrait penser que les éléments utilisés dans le code non managé sont des ressources non managées, mais ce n'est pas vraiment vrai. Un grand nombre de code non managé n'utilise aucune ressource et certains types de ressources non gérées, comme les événements, n'existent que dans l'univers de code managé.
supercat
1
Si un objet de courte durée s'abonne à un événement à partir d'un objet de longue durée (par exemple, il demande à être notifié de tout changement qui se produit pendant la durée de vie de l'objet de courte durée), un tel événement doit être considéré comme une ressource non gérée, car l'échec de désabonner l'événement entraînerait l'extension de la durée de vie de l'objet éphémère à celle de l'objet longue durée. Si plusieurs milliers ou millions d'objets de courte durée souscrivaient à un événement mais étaient abandonnés sans se désabonner, cela pourrait entraîner une fuite de mémoire ou de processeur (car le temps requis pour traiter chaque abonnement augmenterait).
supercat
1
Un autre scénario impliquant des ressources non gérées dans du code managé serait l'allocation d'objets à partir de pools. Surtout si le code doit s'exécuter dans le .NET Micro Framework (dont le ramasse-miettes est beaucoup moins efficace que celui des ordinateurs de bureau), il peut être utile que le code ait par exemple un tableau de structures, dont chacune peut être marquée «utilisée» ou "gratuit". Une demande d'allocation doit trouver une structure qui est actuellement marquée «libre», la marquer «utilisée» et lui renvoyer un index; une demande de libération doit marquer une structure comme "libre". Si une demande d'allocation renvoie par exemple 23, alors ...
supercat
1
... si le code n'indique jamais au propriétaire du tableau qu'il n'a plus besoin de l'élément # 23, cet emplacement de tableau ne sera jamais utilisable par aucun autre code. Une telle allocation manuelle hors des emplacements de tableau n'est pas très souvent utilisée dans le code de bureau car le GC est assez efficace, mais dans le code exécuté sur le Micro Framework, cela peut faire une énorme différence.
supercat
8

La seule chose qui devrait être dans un destructeur C # est cette ligne:

Dispose(False);

C'est tout. Rien d'autre ne devrait jamais être dans cette méthode.

Jonathan Allen
la source
3
Il s'agit du modèle de conception proposé par Microsoft dans la documentation .NET, mais ne l'utilisez pas lorsque votre objet n'est pas IDisposable. msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.110%29.aspx
Zbyl
1
Je ne vois aucune raison d'offrir une classe avec un finaliseur qui n'a pas également de méthode Dispose.
Jonathan Allen
4

Votre question de savoir si vous devez toujours appeler ou non Dispose est généralement un débat houleux. Consultez ce blog pour une perspective intéressante de personnalités respectées de la communauté .NET.

Personnellement, je pense que la position de Jeffrey Richter selon laquelle l'appel Disposen'est pas obligatoire est incroyablement faible. Il donne deux exemples pour justifier son opinion.

Dans le premier exemple, il dit appeler Dispose aux contrôles Windows Forms est fastidieux et inutile dans les scénarios traditionnels. Cependant, il omet de mentionner qu'il Disposeest appelé automatiquement par les conteneurs de contrôle dans ces scénarios traditionnels.

Dans le deuxième exemple, il déclare qu'un développeur peut supposer à tort que l'instance de IAsyncResult.WaitHandledoit être supprimée de manière agressive sans se rendre compte que la propriété initialise paresseusement le handle d'attente, ce qui entraîne une pénalité de performances inutile. Mais, le problème avec cet exemple est que le IAsyncResultlui-même n'adhère pas aux propres directives publiées par Microsoft pour traiter les IDisposableobjets. Autrement dit, si une classe contient une référence à un IDisposabletype, la classe elle-même devrait l'implémenter IDisposable. Si IAsyncResultcette règle était suivie, sa propre Disposeméthode pourrait prendre la décision concernant lequel de ses membres constituants doit être éliminé.

Donc, à moins que quelqu'un n'ait un argument plus convaincant, je vais rester dans le camp "toujours appeler Dispose" avec la compréhension qu'il va y avoir des cas marginaux qui découlent principalement de mauvais choix de conception.

Brian Gideon
la source
3

C'est vraiment assez simple. Je sais que cela a été répondu, mais je vais essayer à nouveau, mais je vais essayer de rester aussi simple que possible.

Un destructeur ne doit généralement jamais être utilisé. Il est uniquement exécuté .net veut qu'il fonctionne. Il ne fonctionnera qu'après un cycle de collecte des ordures. Il se peut qu'il ne soit jamais exécuté pendant le cycle de vie de votre application. Pour cette raison, vous ne devez jamais mettre de code dans un destructeur qui «doit» être exécuté. Vous ne pouvez pas non plus compter sur l'existence d'objets existants dans la classe lors de son exécution (ils ont peut-être déjà été nettoyés car l'ordre dans lequel les destructeurs s'exécutent n'est pas garanti).

IDisposible doit être utilisé chaque fois que vous avez un objet qui crée des ressources à nettoyer (c'est-à-dire des descripteurs de fichiers et graphiques). En fait, beaucoup soutiennent que tout ce que vous mettez dans un destructeur devrait être putin IDisposable pour les raisons énumérées ci-dessus.

La plupart des classes appelleront disposer lorsque le finaliseur est exécuté, mais c'est simplement là comme une sécurité et ne doit jamais être invoqué. Vous devez explicitement supprimer tout ce qui implémente IDisposable lorsque vous en avez terminé. Si vous implémentez IDisposable, vous devez appeler dispose dans le finaliseur. Voir http://msdn.microsoft.com/en-us/library/system.idisposable.aspx pour un exemple.

DaEagle
la source
Non, le garbage collector n'appelle jamais Dispose (). Il n'appelle le finaliseur.
Marc Gravell
Correction de ça. Les classes sont censées appeler disposer dans leur finaliseur, mais elles ne sont pas obligées de le faire.
DaEagle