Finaliseurs (Guide de programmation C#)

Les finaliseurs (historiquement appelés destructeurs) sont utilisés pour effectuer le nettoyage final nécessaire lorsqu’une instance de classe est collectée par le récupérateur de mémoire. Dans la plupart des cas, vous pouvez éviter d’écrire un finaliseur à l’aide des classes System.Runtime.InteropServices.SafeHandle ou dérivées pour envelopper tout descripteur non géré.

Remarques

  • Les finaliseurs ne peuvent pas être définis dans des structs. Ils sont utilisés uniquement avec les classes.
  • Une classe ne peut avoir qu’un seul finaliseur.
  • Les finaliseurs ne peuvent pas être hérités ou surchargés.
  • Les finaliseurs ne peuvent pas être appelés. Ils sont appelés automatiquement.
  • Un finaliseur ne prend pas de modificateur et n’a pas de paramètre.

Par exemple, voici une déclaration de finaliseur pour la classe Car.

class Car
{
    ~Car()  // finalizer
    {
        // cleanup statements...
    }
}

Un finaliseur peut aussi être implémenté en tant que définition de corps d’expression, comme l’illustre l’exemple suivant.

public class Destroyer
{
   public override string ToString() => GetType().Name;

   ~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");
}

Le finaliseur appelle implicitement Finalize sur la classe de base de l’objet. Ainsi, un appel à un finaliseur est traduit implicitement en ce code :

protected override void Finalize()
{
    try
    {
        // Cleanup statements...
    }
    finally
    {
        base.Finalize();
    }
}

Cette conception signifie que la méthode Finalize est appelée de manière récursive pour toutes les instances de la chaîne d’héritage, de la plus dérivée à la moins dérivée.

Notes

Les finaliseurs vides ne doivent pas être utilisés. Quand une classe contient un finaliseur, une entrée est créée dans la file d’attente Finalize. Cette file d’attente est traitée par le récupérateur de mémoire. Lorsque le GC traite la file d’attente, il appelle chaque finaliseur. Les finaliseurs inutiles, y compris les finaliseurs vides, les finaliseurs qui appellent uniquement le finaliseur de classe de base ou les finaliseurs qui appellent uniquement des méthodes émises sous conditions, provoquent une perte inutile de performances.

Le programmeur n’a aucun contrôle sur le moment où le finaliseur est appelé ; le récupérateur de mémoire décide quand l’appeler. Le récupérateur de mémoire recherche les objets qui ne sont plus utilisés par l’application. S’il considère qu’un objet peut être finalisé, il appelle le finaliseur (s’il y en a un) et libère la mémoire utilisée pour stocker l’objet. Il est possible de forcer le nettoyage de mémoire en appelant Collect, mais la plupart du temps, cet appel doit être évité, car il peut créer des problèmes de performances.

Notes

L'exécution ou non des finaliseurs dans le cadre de l’arrêt de l’application est spécifique à chaque implémentation de .NET. Lorsqu’une application se termine, .NET Framework effectue tous les efforts raisonnables pour appeler les finaliseurs des objets qui n’ont pas encore été collectés par la mémoire, à moins que ce nettoyage n'ait été supprimé (par un appel à la méthode GC.SuppressFinalizede bibliothèque, par exemple). .NET 5 (y compris .NET Core) et les versions ultérieures n’appellent pas de finaliseurs dans le cadre de l’arrêt de l’application. Pour plus d’informations, consultez le problème GitHub dotnet/csharpstandard #291.

Si vous devez effectuer un nettoyage fiable lors de la sortie d’une application, inscrivez un gestionnaire pour l’événement System.AppDomain.ProcessExit. Ce gestionnaire garantit que IDisposable.Dispose() (ou, IAsyncDisposable.DisposeAsync()) a été appelé pour tous les objets qui nécessitent un nettoyage avant la sortie de l’application. Étant donné que vous ne pouvez pas appeler Finalize directement et que vous ne pouvez pas garantir que le récupérateur de mémoire appelle tous les finaliseurs avant la sortie, vous devez utiliser Dispose ou DisposeAsync pour vous assurer que les ressources sont libérées.

Utiliser des finaliseurs pour libérer des ressources

En règle générale, C# ne nécessite pas autant de gestion de la mémoire de la part du développeur que les langages qui ne ciblent pas un runtime avec nettoyage de la mémoire. Cela est dû au fait que le récupérateur de mémoire .NET gère implicitement l’allocation et la libération de mémoire pour vos objets. Toutefois, quand votre application encapsule des ressources non gérées, telles que des fenêtres, des fichiers et des connexions réseau, vous devez utiliser des finaliseurs pour libérer ces ressources. Quand l’objet peut être finalisé, le récupérateur de mémoire exécute la méthode Finalize de l’objet.

Libération explicite de ressources

Si votre application utilise une ressource externe coûteuse, nous vous recommandons également de proposer un moyen de libérer explicitement la ressource avant que le récupérateur de mémoire ne libère l’objet. Pour libérer la ressource, implémentez une méthode Dispose à partir de l’interface IDisposable qui effectue le nettoyage nécessaire pour l’objet. Cela peut améliorer considérablement les performances de l’application. Même avec ce contrôle explicite sur les ressources, le finaliseur devient une protection pour nettoyer les ressources si l’appel à la méthode Dispose échoue.

Pour plus d’informations sur le nettoyage des ressources, consultez les articles suivants :

Exemple

L’exemple suivant crée trois classes qui forment une chaîne d’héritage. La classe First est la classe de base, Second est dérivée de First, et Third est dérivée de Second. Toutes trois ont des finaliseurs. Dans Main, une instance de la classe la plus dérivée est créée. La sortie de ce code dépend de l’implémentation de .NET que l'application cible :

  • .NET Framework : la sortie indique que les finaliseurs pour les trois classes sont appelés automatiquement lorsque l’application se termine, dans l’ordre du plus dérivé au moins dérivé.
  • .NET 5 (y compris .NET Core) ou une version ultérieure : il n’existe aucune sortie, car cette implémentation de .NET n’appelle pas de finaliseurs lorsque l’application se termine.
class First
{
    ~First()
    {
        System.Diagnostics.Trace.WriteLine("First's finalizer is called.");
    }
}

class Second : First
{
    ~Second()
    {
        System.Diagnostics.Trace.WriteLine("Second's finalizer is called.");
    }
}

class Third : Second
{
    ~Third()
    {
        System.Diagnostics.Trace.WriteLine("Third's finalizer is called.");
    }
}

/* 
Test with code like the following:
    Third t = new Third();
    t = null;

When objects are finalized, the output would be:
Third's finalizer is called.
Second's finalizer is called.
First's finalizer is called.
*/

spécification du langage C#

Pour plus d’informations, consultez la section Finaliseurs de la Spécification du langage C#.

Voir aussi