La gestion de la mémoire et la gestion des ressources sont deux concepts qu'il est facile de confondre lorsqu'on débute dans le développement logiciel. Il est important de connaître la différence entre ces deux concepts pour écrire des logiciels fiables et maintenables. 

 

Cet article vise à clarifier ces deux concepts et à dissiper toute confusion qui pourrait subsister.

 

 

Gestion de la mémoire

La mémoire est une ressource managée, ce qui signifie qu'elle est directement contrôlée par le Runtime. Lorsqu'un objet n'est plus référencé, le garbage collector le supprime automatiquement de la mémoire. Nous n'avons donc pas besoin de nous soucier de libérer explicitement la mémoire.

Il est important de noter que, bien que vous puissiez appeler manuellement la méthode statique GC.Collect() pour demander au garbage collector de libérer la mémoire, cela est généralement déconseillé  car cela pourrait causer plus de tort que de bien.

Les détails exacts du processus de nettoyage ne sont pas couverts dans cet article, mais en résumé : les objets dans la mémoire heap passent par une phase de marquage, où les objets encore référencés sont "marqués" pour ne pas être collectés par le garbage collector. Ensuite, nous passons à la deuxième phase, où les objets non marqués sont collectés.

 

 

Gestion des ressources

D'autre part, les ressources telles que les handles de fichiers ou les connexions ouvertes sont considérées comme des ressources non gérées, elles s'exécutent en dehors du Runtime .NET. Ces ressources ne sont pas automatiquement "managées" par le CLR (Common Language Runtime) et doivent donc être gérées par le développeur.

La méthode principale pour gérer ces ressources en .NET est d'implémenter l'interface "IDisposable" (nous en discuterons plus en détail dans la suite de cet article). De cette façon, le développeur sait qu'il doit explicitement appeler la méthode Dispose() pour libérer la ressource. C'est ce que l'on appelle la gestion déterministe des ressources.


En outre, il est recommandé d'utiliser le mot-clé "using" lorsque l'on tente de se débarrasser d'un objet, afin de s'assurer que la méthode "Dispose" est appelée même si une exception se produit dans le bloc. Dans le cas contraire, il convient de placer l'objet dans un "try catch" et d'appeler Dispose() dans le bloc finally.

 

using (var disposableObject = new SomeDisposableObject())
{
// Code that uses disposableObject
}

Au lieu de :


var disposableObject = new SomeDisposableObject();
try
{
// Code that uses disposableObject
}
catch (Exception ex)
{
}
finally
{
    disposableObject.Dispose();
}

 

Les destructeurs, également appelés finaliseurs, constituent une autre méthode de libération des ressources. Les finaliseurs sont invoqués lors de la phase de garbage collection lorsque le consommateur ne libère pas correctement un objet possédant des ressources non managées. Ceci est connu sous le nom de gestion non déterministe des ressources, car l'utilisateur ne sait pas exactement quand la ressource sera libérée.
 
Il est crucial de noter que l'utilisation des finaliseurs introduit une charge de calcul supplémentaire et est généralement à éviter.

 

 

Implémentation du modèle “IDisposable” 

La raison pour laquelle j'ai laissé de côté l'implémentation du pattern IDisposable jusqu'à présent est que nous devions d'abord examiner les finaliseurs.
 
En général, il existe deux cas de figure :
 
  • Vous avez uniquement une classe contenant d'autres ressources managées implémentant IDisposable (flux de fichiers, connexions de base de données, etc.). C'est le scénario le plus courant dans la plupart des cas.
  • Vous avez une classe qui détient directement une ressource non gérée (des ressources qui appartiennent au système d'exploitation, comme des handles de fichiers, des handles de fenêtres, entre autres).


Premier cas

Sa mise en application est simple :

 

public class MyResource : IDisposable
{
    public void Dispose()
    {
            // Clean up managed resources, call Dispose on member variables..
    }

 

Deuxième cas

Nous devons implémenter un finaliseur, mais avant d'aborder ce sujet, il est important de souligner que le besoin d'implémenter explicitement un finaliseur et de gérer directement des ressources non gérées est relativement rare. La plupart du temps, nous utilisons des types existants (comme FileStream, HttpClient...) qui gèrent déjà correctement la gestion des ressources.

Ainsi, à moins que vous ne mainteniez un code legacy qui implique de traiter d'anciennes API ou de travailler sur une bibliothèque de bas niveau où la gestion directe des ressources est nécessaire, vous n'aurez probablement à faire face qu'au premier cas.

En ce qui concerne le code pour l'implémentation d'un finaliseur :

 

public class MyResource : IDisposable
{
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Clean up managed resources, dispose other IDisposable objects here
            }

            // Clean up unmanaged resources (release native resources, file handles etc)

            _disposed = true;
        }
    }

    // Finalizer
    ~MyResource()
    {
        Dispose(false);
    }
}

 

Quelques points à noter à ce sujet:

  • Lorsque Dispose(true)  est appelé, cela indique que l'objet est explicitement éliminé avec la méthode Dispose(). Dans ce cas, nous devons libérer les ressources managées et non managées.
  • Comme expliqué précédemment, le finaliseur s'exécute pendant le garbage collecting comme un filet de sécurité au cas où le consommateur de la classe n'appellerait pas Dispose() explicitement. Nous y appelons Dispose(false) pour libérer uniquement les ressources non gérées. En passant false, nous évitons de réintroduire le code géré, évitant ainsi des problèmes potentiels tels que l'accès à des objets managés éliminés.
  • Nous appelons GC.SuppressFinalize(this) dans la méthode Dispose() GC.SuppressFinalize(this)  pour informer le garbage collector que le finaliseur de cet objet ne doit pas être exécuté, évitant ainsi une surcharge inutile pendant le garbage collecting.

Remarque: il est essentiel de suivre ce pattern pour éviter les fuites de ressources et garantir un nettoyage adéquat.

 

 

Pour une approche plus approfondie...
Partager cet article