Le but de Dispose est de libérer des ressources non managées. Cela doit être fait à un moment donné, sinon ils ne seront jamais nettoyés. Le garbage collector ne sait pas comment appeler DeleteHandle()
à une variable de type IntPtr
, il ne sait pas si oui ou non il doit appeler DeleteHandle()
.
Remarque : Qu'est-ce qu'une ressource non gérée ? Si vous l'avez trouvé dans Microsoft .NET Framework: c'est géré. Si vous avez vous-même fouillé MSDN, ce n'est pas géré. Tout ce que vous avez utilisé les appels P / Invoke pour sortir du monde agréable et confortable de tout ce qui est disponible dans le .NET Framework n'est pas géré - et vous êtes maintenant responsable de le nettoyer.
L'objet que vous avez créé doit exposer une méthode, que le monde extérieur peut appeler, afin de nettoyer les ressources non gérées. La méthode peut être nommée comme vous le souhaitez:
public void Cleanup()
ou
public void Shutdown()
Mais à la place, il existe un nom normalisé pour cette méthode:
public void Dispose()
Il y a même eu une interface créée IDisposable
, qui n'a qu'une seule méthode:
public interface IDisposable
{
void Dispose()
}
Donc, vous faites exposer votre objet à l' IDisposable
interface, et de cette façon vous promettez que vous avez écrit cette seule méthode pour nettoyer vos ressources non managées:
public void Dispose()
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}
Et tu as fini. Sauf que vous pouvez faire mieux.
Que faire si votre objet a alloué un System.Drawing.Bitmap de 250 Mo (c'est-à-dire la classe Bitmap gérée .NET) comme une sorte de tampon de trame? Bien sûr, il s'agit d'un objet .NET géré et le garbage collector le libérera. Mais voulez-vous vraiment laisser 250 Mo de mémoire juste assis là - en attendant que le ramasse-miettes finisse par venir le libérer? Et s'il y a une connexion de base de données ouverte ? Certes, nous ne voulons pas que cette connexion reste ouverte, attendant que le GC finalise l'objet.
Si l'utilisateur a appelé Dispose()
(ce qui signifie qu'il ne prévoit plus d'utiliser l'objet), pourquoi ne pas se débarrasser de ces bitmaps et connexions de base de données inutiles?
Alors maintenant, nous allons:
- se débarrasser des ressources non gérées (parce que nous devons le faire), et
- se débarrasser des ressources gérées (parce que nous voulons être utiles)
Mettons donc à jour notre Dispose()
méthode pour nous débarrasser de ces objets gérés:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
Et tout va bien, sauf que vous pouvez faire mieux !
Et si la personne a oublié d'appeler Dispose()
votre objet? Ensuite, ils fuiraient des ressources non gérées !
Remarque: Ils ne laisseront pas fuir les ressources gérées , car finalement le garbage collector va s'exécuter, sur un thread d'arrière-plan, et libérer la mémoire associée aux objets inutilisés. Cela inclura votre objet et tous les objets gérés que vous utilisez (par exemple le Bitmap
et le DbConnection
).
Si la personne a oublié d'appeler Dispose()
, nous pouvons toujours conserver son bacon! Nous avons encore un moyen de l'appeler pour eux: quand le garbage collector se met enfin à libérer (c'est-à-dire finaliser) notre objet.
Remarque: Le garbage collector libérera éventuellement tous les objets gérés. Dans ce cas, il appelle la Finalize
méthode sur l'objet. Le GC ne connaît pas ou ne se soucie pas de votre méthode Dispose . C'était juste un nom que nous avons choisi pour une méthode que nous appelons lorsque nous voulons nous débarrasser des trucs non gérés.
La destruction de notre objet par le garbage collector est le moment idéal pour libérer ces satanées ressources non gérées. Nous le faisons en remplaçant la Finalize()
méthode.
Remarque: En C #, vous ne remplacez pas explicitement la Finalize()
méthode. Vous écrivez une méthode qui ressemble à un destructeur C ++ , et le compilateur prend cela pour votre implémentation de la Finalize()
méthode:
~MyObject()
{
//we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
Dispose(); //<--Warning: subtle bug! Keep reading!
}
Mais il y a un bug dans ce code. Vous voyez, le garbage collector s'exécute sur un thread d'arrière - plan ; vous ne connaissez pas l'ordre dans lequel deux objets sont détruits. Il est tout à fait possible que dans votre Dispose()
code, l' objet géré dont vous essayez de vous débarrasser (parce que vous vouliez être utile) ne soit plus là:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
//Free managed resources too
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
this.frameBufferImage = null;
}
}
Donc, ce dont vous avez besoin est un moyen Finalize()
de dire Dispose()
qu'il ne devrait toucher aucune ressource gérée (car ils pourraient ne plus y être ), tout en libérant des ressources non gérées.
Le modèle standard pour ce faire est d'avoir Finalize()
et les Dispose()
deux appellent une troisième (!) Méthode; où vous passez un dicton booléen si vous l'appelez depuis Dispose()
(par opposition à Finalize()
), ce qui signifie qu'il est sûr de libérer des ressources gérées.
Cette méthode interne peut recevoir un nom arbitraire comme "CoreDispose" ou "MyInternalDispose", mais il est de tradition de l'appeler Dispose(Boolean)
:
protected void Dispose(Boolean disposing)
Mais un nom de paramètre plus utile pourrait être:
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too, but only if I'm being called from Dispose
//(If I'm being called from Finalize then the objects might not exist
//anymore
if (itIsSafeToAlsoFreeManagedObjects)
{
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
}
Et vous changez votre implémentation de la IDisposable.Dispose()
méthode en:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
}
et votre finaliseur pour:
~MyObject()
{
Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}
Remarque : Si votre objet descend d'un objet qui implémente Dispose
, n'oubliez pas d'appeler leur méthode Dispose de base lorsque vous substituez Dispose:
public override void Dispose()
{
try
{
Dispose(true); //true: safe to free managed resources
}
finally
{
base.Dispose();
}
}
Et tout va bien, sauf que vous pouvez faire mieux !
Si l'utilisateur appelle Dispose()
votre objet, alors tout a été nettoyé. Plus tard, lorsque le garbage collector arrive et appelle Finalize, il appelle à Dispose
nouveau.
Non seulement cela est un gaspillage, mais si votre objet a des références indésirables à des objets que vous avez déjà éliminés depuis le dernier appel Dispose()
, vous essaierez de les éliminer à nouveau!
Vous remarquerez dans mon code que j'ai pris soin de supprimer les références aux objets que j'ai supprimés, donc je n'essaie pas d'appeler Dispose
une référence d'objet indésirable. Mais cela n'a pas empêché un bug subtil de s'introduire.
Lorsque l'utilisateur appelle Dispose()
: le handle CursorFileBitmapIconServiceHandle est détruit. Plus tard, lorsque le garbage collector s'exécute, il essaiera de détruire à nouveau la même poignée.
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy
...
}
La façon de résoudre ce problème est de dire au garbage collector qu'il n'a pas besoin de se soucier de finaliser l'objet - ses ressources ont déjà été nettoyées et aucun travail supplémentaire n'est nécessaire. Pour ce faire, appelez GC.SuppressFinalize()
la Dispose()
méthode:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}
Maintenant que l'utilisateur a appelé Dispose()
, nous avons:
- libéré des ressources non gérées
- ressources gérées libérées
Il est inutile que le GC exécute le finaliseur - tout est réglé.
Ne puis-je pas utiliser Finalize pour nettoyer les ressources non gérées?
La documentation de Object.Finalize
dit:
La méthode Finalize est utilisée pour effectuer des opérations de nettoyage sur les ressources non gérées détenues par l'objet actuel avant que l'objet ne soit détruit.
Mais la documentation MSDN indique également, pour IDisposable.Dispose
:
Effectue des tâches définies par l'application associées à la libération, à la libération ou à la réinitialisation des ressources non gérées.
Alors c'est quoi? Lequel est l'endroit pour moi de nettoyer les ressources non gérées? La réponse est:
C'est ton choix! Mais choisissez Dispose
.
Vous pouvez certainement placer votre nettoyage non géré dans le finaliseur:
~MyObject()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//A C# destructor automatically calls the destructor of its base class.
}
Le problème est que vous n'avez aucune idée du moment où le garbage collector se mettra à finaliser votre objet. Vos ressources natives non gérées, non nécessaires et non utilisées resteront en place jusqu'à ce que le garbage collector s'exécute finalement . Ensuite, il appellera votre méthode de finalisation; nettoyer les ressources non gérées. La documentation d' Object.Finalize le souligne:
L'heure exacte à laquelle le finaliseur s'exécute n'est pas définie. Pour garantir une libération déterministe des ressources pour les instances de votre classe, implémentez une méthode Close ou fournissez une IDisposable.Dispose
implémentation.
C'est la vertu de l'utilisation Dispose
pour nettoyer les ressources non gérées; vous apprenez et contrôlez le nettoyage des ressources non gérées. Leur destruction est "déterministe" .
Pour répondre à votre question initiale: pourquoi ne pas libérer de la mémoire maintenant plutôt que lorsque le GC décide de le faire? J'ai un logiciel de reconnaissance faciale besoins pour se débarrasser de 530 Mo d'images internes maintenant , car ils ne sont plus nécessaires. Quand nous ne le faisons pas: la machine s'arrête brusquement.
Lecture bonus
Pour tous ceux qui aiment le style de cette réponse (en expliquant le pourquoi , donc le comment devient évident), je vous suggère de lire le premier chapitre du COM essentiel de Don Box:
En 35 pages, il explique les problèmes d'utilisation des objets binaires et invente COM devant vos yeux. Une fois que vous comprenez le pourquoi de COM, les 300 pages restantes sont évidentes et détaillent simplement l'implémentation de Microsoft.
Je pense que tout programmeur qui a déjà traité d'objets ou de COM devrait, à tout le moins, lire le premier chapitre. C'est la meilleure explication de tout.
Lecture bonus supplémentaire
Quand tout ce que vous savez est faux par Eric Lippert
Il est donc très difficile d'écrire un finaliseur correct, et le meilleur conseil que je puisse vous donner est de ne pas essayer .