web-dev-qa-db-fra.com

Avez-vous besoin de disposer d'objets et de les définir sur null?

Avez-vous besoin de supprimer des objets et de les définir sur null, ou le ramasse-miettes va-t-il les nettoyer lorsqu'ils disparaissent?

288
CJ7

Les objets seront nettoyés quand ils ne seront plus utilisés et quand le ramasse-miettes jugera bon de le faire. Parfois, vous devrez peut-être définir un objet sur null pour le rendre hors de son champ d'application (tel qu'un champ statique dont vous n'avez plus besoin), mais dans l'ensemble, il n'est généralement pas nécessaire de définir sur null.

En ce qui concerne l'élimination des objets, je suis d'accord avec @Andre. Si l'objet est IDisposable c'est ne bonne idée de le disposer lorsque vous n'en avez plus besoin, en particulier si l'objet utilise des ressources non gérées. Ne pas disposer de ressources non gérées va entraîner des fuites de mémoire.

Vous pouvez utiliser l'instruction using pour supprimer automatiquement un objet une fois que votre programme a quitté la portée de l'instruction using.

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

Ce qui est fonctionnellement équivalent à:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}
228
Zach Johnson

Les objets ne sortent jamais de la portée en C # comme ils le font en C++. Ils sont traités automatiquement par le ramasse-miettes lorsqu'ils ne sont plus utilisés. C'est une approche plus compliquée que C++ où la portée d'une variable est entièrement déterministe. Le ramasse-miettes CLR parcourt activement tous les objets créés et détermine s’ils sont utilisés.

Un objet peut sortir "hors de portée" dans une fonction, mais si sa valeur est renvoyée, alors GC vérifiera si la fonction appelante conserve ou non la valeur de retour.

Il n'est pas nécessaire de définir des références d'objet sur null, car le garbage collection fonctionne en déterminant quels objets sont référencés par d'autres objets.

En pratique, vous n'avez pas à vous soucier de la destruction, cela fonctionne et c'est génial :)

Dispose doit être appelé sur tous les objets qui implémentent IDisposable lorsque vous avez fini de les utiliser. Normalement, vous utiliseriez un bloc using avec ces objets comme ceci:

using (var ms = new MemoryStream()) {
  //...
}

EDIT Sur une portée variable. Craig a demandé si la portée de la variable avait un effet sur la durée de vie de l'objet. Pour bien expliquer cet aspect du CLR, il me faudra expliquer quelques concepts de C++ et C #.

Portée de la variable réelle

Dans les deux langues, la variable ne peut être utilisée que dans la même portée que celle définie - classe, fonction ou bloc d'instructions entouré par des accolades. La différence subtile, cependant, est que dans C #, les variables ne peuvent pas être redéfinies dans un bloc imbriqué.

En C++, c'est parfaitement légal:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

En C #, cependant, vous obtenez une erreur de compilation:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

Cela a du sens si vous regardez MSIL généré - toutes les variables utilisées par la fonction sont définies au début de la fonction. Jetez un oeil à cette fonction:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

Ci-dessous, l'IL généré. Notez que iVal2, qui est défini à l'intérieur du bloc if, est en fait défini au niveau de la fonction. En pratique, cela signifie que C # n'a qu'une étendue de classe et de niveau de fonction en ce qui concerne la durée de vie variable.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

Portée C++ et durée de vie de l'objet

Chaque fois qu'une variable C++, allouée sur la pile, sort de la portée, elle est détruite. N'oubliez pas qu'en C++, vous pouvez créer des objets sur la pile ou sur le tas. Lorsque vous les créez sur la pile, une fois que l'exécution quitte la portée, elles sont éjectées de la pile et détruites.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

Lorsque des objets C++ sont créés sur le tas, ils doivent être explicitement détruits, sinon il s'agit d'une fuite de mémoire. Pas de problème avec les variables de pile cependant.

Durée de vie de l'objet C #

Dans CLR, les objets (c'est-à-dire les types de référence) sont toujours créés sur le segment de mémoire géré. Ceci est encore renforcé par la syntaxe de création d'objet. Considérons cet extrait de code.

MyClass stackObj;

En C++, cela créerait une instance sur MyClass sur la pile et appellerait son constructeur par défaut. En C #, cela créerait une référence à la classe MyClass qui ne pointe pas vers rien. La seule façon de créer une instance d'une classe consiste à utiliser l'opérateur new:

MyClass stackObj = new MyClass();

D'une certaine manière, les objets C # ressemblent beaucoup aux objets créés à l'aide de la syntaxe new en C++ - ils sont créés sur le tas, mais contrairement aux objets C++, ils sont gérés par le moteur d'exécution, vous n'avez donc pas à vous inquiéter. de les détruire.

Étant donné que les objets sont toujours sur le tas, le fait que les références d’objet (c’est-à-dire les pointeurs) ne soient plus à la portée devient sans objet. Pour déterminer si un objet doit être collecté, il y a plus de facteurs que la simple présence de références à l'objet.

Références d'objet C #

Jon Skeet références d'objet comparées en Java à des morceaux de chaîne attachés à la bulle, qui est l'objet. La même analogie s'applique aux références d'objet C #. Ils indiquent simplement un emplacement du segment contenant l'objet. Ainsi, la définition de la valeur null n'a pas d'effet immédiat sur la durée de vie de l'objet. Le ballon continue d'exister jusqu'à ce que le GC le "fasse apparaître".

En poursuivant l'analogie avec le ballon, il semblerait logique qu'une fois que le ballon n'a plus aucune condition, il peut être détruit. En fait, c'est exactement comment les objets comptés par référence fonctionnent dans des langages non gérés. Sauf que cette approche ne fonctionne pas très bien pour les références circulaires. Imaginez deux ballons qui sont attachés ensemble par une ficelle, mais aucun ballon n'a de ficelle. Sous de simples règles de comptage des références, ils continuent d'exister, même si tout le groupe de ballons est "orphelin".

Les objets .NET ressemblent beaucoup à des ballons à l'hélium placés sous un toit. Lorsque le toit s'ouvre (GC fonctionne), les ballons non utilisés s'éloignent, même s'il peut y avoir des groupes de ballons attachés ensemble.

.NET GC utilise une combinaison de GC générationnelle, de marquage et de balayage. L'approche générationnelle implique que le moteur d'exécution privilégie l'inspection des objets alloués récemment, car ils sont plus susceptibles d'être inutilisés et de marquer et balayer implique que le moteur d'exécution examine l'ensemble du graphe d'objets et détermine s'il existe des groupes d'objets inutilisés. Ceci traite de manière adéquate le problème de dépendance circulaire.

En outre, .NET GC s'exécute sur un autre thread (dénommé thread de finaliseur) car il a beaucoup à faire et cela sur le thread principal interromprait votre programme.

130
Igor Zevaka

Comme d'autres l'ont déjà dit, vous voulez absolument appeler Dispose si la classe implémente IDisposable. Je prends une position assez rigide à ce sujet. Certains pourraient prétendre qu'appeler Dispose sur DataSet, par exemple, est inutile car ils l'ont démonté et ont constaté qu'il ne faisait rien de significatif. Mais, je pense qu'il y a de nombreuses erreurs dans cet argument.

Lisez this pour un débat intéressant mené par des personnes respectées sur le sujet. Ensuite, lisez mon raisonnement ici pourquoi je pense que Jeffery Richter est dans le mauvais camp.

Maintenant, indiquez si vous devez ou non définir une référence à null. La réponse est non. Laissez-moi illustrer mon propos avec le code suivant.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}

Alors, quand pensez-vous que l'objet référencé par a est éligible pour la collecte? Si vous avez dit après l'appel à a = null alors vous vous trompez. Si vous dites après la fin de la méthode Main, vous vous trompez également. La réponse correcte est qu'il est éligible pour la collecte à un moment donné pendant l'appel à DoSomething. C'est vrai. Il est éligible avant la référence est définie sur null et peut-être même avant que l'appel à DoSomething ne soit terminé. En effet, le compilateur JIT peut reconnaître que les références aux objets ne sont plus déréférencées, même si elles sont toujours enracinées.

17
Brian Gideon

Vous n'avez jamais besoin de définir des objets sur null en C #. Le compilateur et le runtime se chargeront de déterminer quand ils ne sont plus dans la portée.

Oui, vous devez vous débarrasser des objets qui implémentent IDisposable.

13
EMP

Je suis d'accord avec la réponse commune ici: oui, vous devez disposer et non, vous ne devez généralement pas définir la variable sur null ... mais je tiens à souligner que l'élimination ne concerne PAS principalement la gestion de la mémoire. Oui, cela peut aider (et parfois aide) à la gestion de la mémoire, mais son objectif principal est de vous permettre de libérer de manière déterministe des ressources rares.

Par exemple, si vous ouvrez un port matériel (série, par exemple), un socket TCP/IP, un fichier (en mode d’accès exclusif) ou même une connexion à une base de données, vous évitez maintenant que tout autre code utilise ces éléments jusqu’à leur libération. Dispose libère généralement ces éléments (avec GDI et d'autres descripteurs "os", etc. dont il existe des milliers disponibles, mais qui restent globalement limités). Si vous n'appelez pas dipose sur l'objet propriétaire et ne libérez pas explicitement ces ressources, essayez de rouvrir ultérieurement la même ressource (ou un autre programme), cette tentative d'ouverture échouera, car l'objet est toujours non ouvert et non collecté. . Bien sûr, lorsque le GC collecte l’élément (si le modèle Dispose a été mis en œuvre correctement), la ressource est libérée ... mais vous ne savez pas quand cela le sera, vous ne savez donc pas quand il est sûr ouvrir cette ressource. C’est le principal problème que Dispose a résolu. Bien sûr, relâcher ces poignées libère souvent aussi de la mémoire, et ne jamais les relâcher peut ne jamais libérer cette mémoire ... d'où tout le discours sur les fuites de mémoire ou les retards dans le nettoyage de la mémoire.

J'ai vu des exemples concrets de ce problème causant. Par exemple, j’ai vu des applications Web ASP.Net dont la connexion à la base de données échouait éventuellement (bien que pendant de courtes périodes ou jusqu’à ce que le processus du serveur Web soit redémarré) car le pool de connexions du serveur SQL était saturé ... c.-à-d. , de nombreuses connexions ont été créées et ne sont pas explicitement libérées en si peu de temps qu’aucune nouvelle connexion ne peut être créée et que de nombreuses connexions du pool, bien qu’elles ne soient pas actives, sont toujours référencées par des objets non collectés et non collectés, et peuvent donc ' t être réutilisé. En éliminant correctement les connexions de base de données, le cas échéant, garantit que ce problème ne se produira pas (du moins, à moins que vous n'ayez un très fort accès simultané ).

11
Yort

Si l'objet implémente IDisposable, alors oui, vous devez le disposer. L'objet peut être suspendu à des ressources natives (descripteurs de fichier, objets OS) qui pourraient ne pas être libérés immédiatement sinon. Cela peut entraîner une privation de ressources, des problèmes de verrouillage de fichiers et d’autres bugs subtils qui pourraient autrement être évités.

Voir aussi Implémentation d'une méthode Dispose sur MSDN.

11
Chris Schmich

S'ils implémentent l'interface IDisposable, vous devez les disposer. Le ramasse-miettes se chargera du reste.

EDIT: Le mieux est d’utiliser la commande using lorsque vous travaillez avec des objets jetables:

using(var con = new SqlConnection("..")){ ...
9
Andre

Normalement, il n'est pas nécessaire de définir les champs sur null. Cependant, je recommanderais toujours de disposer de ressources non gérées.

Par expérience, je vous conseillerais également de procéder comme suit:

  • Désabonnez-vous des événements si vous n'en avez plus besoin.
  • Définissez tout champ contenant un délégué ou une expression sur null si vous n'en avez plus besoin.

Il m'est très difficile de trouver des problèmes qui résultent directement du non-respect des conseils ci-dessus.

Un bon endroit pour le faire est dans Dispose (), mais plus tôt c'est généralement mieux.

En général, s'il existe une référence à un objet, le ramasse-miettes (GC) peut prendre quelques générations de plus pour déterminer qu'un objet n'est plus utilisé. Pendant tout ce temps, l'objet reste en mémoire.

Cela peut ne pas être un problème jusqu'à ce que vous constatiez que votre application utilise beaucoup plus de mémoire que prévu. Lorsque cela se produit, connectez un profileur de mémoire pour voir quels objets ne sont pas nettoyés. Définir les champs référençant d'autres objets sur null et effacer les collections lors de leur élimination peut vraiment aider le GC à déterminer quels objets il peut supprimer de la mémoire. Le GC récupérera plus rapidement la mémoire utilisée, rendant ainsi votre application beaucoup moins gourmande en mémoire et plus rapide.

4
Marnix van Valen

Lorsqu'un objet implémente IDisposable, vous devez appeler Dispose (ou Close, dans certains cas, cela appellera Dispose pour vous).

Vous n'avez normalement pas besoin de définir les objets sur null, car le CPG saura qu'un objet ne sera plus utilisé.

Il existe une exception lorsque je règle les objets sur null. Lorsque je récupère un grand nombre d'objets (de la base de données) sur lesquels je dois travailler, et les stocke dans une collection (ou un tableau). Lorsque le "travail" est terminé, je règle l'objet sur null, car le GC ne sait pas que j'ai fini de le travailler.

Exemple:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called
4
GvS

Appelez toujours disposer. Cela ne vaut pas le risque. Les grandes applications d'entreprise gérées doivent être traitées avec respect. Aucune hypothèse ne peut être faite sinon il reviendra vous mordre.

N'écoute pas Leppie.

Beaucoup d'objets n'implémentent pas réellement IDisposable, vous n'avez donc pas à vous en soucier. S'ils sortent véritablement du champ d'application, ils seront automatiquement libérés. Aussi, je n'ai jamais rencontré la situation où j'ai dû mettre quelque chose à null.

Une chose qui peut arriver est que beaucoup d'objets peuvent être tenus ouverts. Cela peut considérablement augmenter l'utilisation de la mémoire de votre application. Il est parfois difficile de déterminer s'il s'agit réellement d'une fuite de mémoire ou si votre application exécute beaucoup de tâches.

Les outils de profil de mémoire peuvent vous aider, mais cela peut être délicat.

De plus, vous devez toujours vous désabonner des événements inutiles. Soyez également prudent avec la liaison et les contrôles WPF. Ce n'est pas une situation habituelle, mais je suis tombé sur une situation où j'avais un contrôle WPF lié à un objet sous-jacent. L'objet sous-jacent était volumineux et occupait une grande quantité de mémoire. Le contrôle WPF était en train d'être remplacé par une nouvelle instance, et l'ancienne était toujours en suspens pour une raison quelconque. Cela a provoqué une fuite de mémoire importante.

Dans hindsite, le code était mal écrit, mais le fait est que vous voulez vous assurer que les choses qui ne sont pas utilisées deviennent hors de portée. Celui-ci a mis beaucoup de temps à trouver avec un profileur de mémoire car il est difficile de savoir ce qui est en mémoire est valide et ce qui ne devrait pas être là.

3
peter

Je dois répondre aussi. Le JIT génère des tables avec le code à partir de son analyse statique de l'utilisation des variables. Ces entrées de table sont les "racines GC" dans le cadre de pile actuel. Au fur et à mesure que le pointeur d'instruction avance, ces entrées de table deviennent non valides et donc prêtes pour le nettoyage de la mémoire. Par conséquent: s'il s'agit d'une variable étendue, vous n'avez pas besoin de la définir sur null, le GC récupérera l'objet. S'il s'agit d'un membre ou d'une variable statique, vous devez le définir sur null

2
Hui

Il y a une bonne discussion sur le sujet (ainsi que l'historique du motif Dispose) dans cet épisode de .NET Rocks!

http://www.dotnetrocks.com/default.aspx?showNum=1

0
Rob Windsor