Finalizadores (Guía de programación de C#)

Los finalizadores (anteriormente conocidos como destructores) se usan para realizar cualquier limpieza final necesaria cuando el recolector de elementos no utilizados recopila una instancia de clase. En la mayoría de los casos, puede evitar escribir un finalizador mediante System.Runtime.InteropServices.SafeHandle o clases derivadas para encapsular cualquier identificador no administrado.

Comentarios

  • Los finalizadores no se pueden definir en structs. Solo se usan con clases.
  • Una clase solo puede tener un finalizador.
  • Los finalizadores no se pueden heredar ni sobrecargar.
  • No se puede llamar a los finalizadores. Se invocan automáticamente.
  • Un finalizador no permite modificadores ni tiene parámetros.

Por ejemplo, el siguiente código muestra una declaración de un finalizador para la clase Car.

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

Un finalizador también puede implementarse como una definición de cuerpo de expresión, como se muestra en el ejemplo siguiente.

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

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

El finalizador llama implícitamente a Finalize en la clase base del objeto. Por lo tanto, una llamada a un finalizador se convierte implícitamente al siguiente código:

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

Este diseño significa que se realizan llamadas al método Finalize de manera recursiva para todas las instancias de la cadena de herencia, desde la más a la menos derivada.

Nota

Los finalizadores vacíos no deben usarse. Cuando una clase contiene un finalizador, se crea una entrada en la cola Finalize. El recolector de elementos no utilizados procesa esta cola. Cuando esto sucede, llama a cada finalizador. Los finalizadores innecesarios, como los vacíos, los que solo llaman al finalizador de clase base o los que solo llaman a métodos emitidos condicionalmente provocan una pérdida innecesaria de rendimiento.

El programador no puede controlar cuándo se llama al finalizador; es el recolector de elementos no utilizados el que decide cuándo hacerlo. El recolector de elementos no utilizados comprueba si hay objetos que ya no están siendo usados por ninguna aplicación. Si considera un objeto elegible para su finalización, llama al finalizador (si existe) y reclama la memoria usada para almacenar el objeto. Es posible forzar la recolección de elementos no utilizados si se llama a Collect, pero debe evitarse esta llamada porque puede dar lugar a problemas de rendimiento.

Nota

El hecho de que los finalizadores se ejecuten o no como parte de la finalización de aplicaciones es específico de cada implementación de .NET. Cuando finaliza una aplicación, .NET Framework hace todo lo posible para llamar a todos los finalizadores en los objetos cuyos elementos no se han detectado durante la recolección de elementos no utilizados, a menos que se haya suprimido esa limpieza (por ejemplo, mediante una llamada al método de biblioteca GC.SuppressFinalize). En .NET 5 (incluido .NET Core) y versiones posteriores no se llama a los finalizadores como parte de la finalización de la aplicación. Para obtener más información, vea la incidencia de GitHub dotnet/csharpstandard n.º 291.

Si necesita realizar la limpieza de forma confiable cuando existe una aplicación, registre un controlador para el evento System.AppDomain.ProcessExit. Ese controlador garantizaría la llamada a IDisposable.Dispose(), o a IAsyncDisposable.DisposeAsync(), para todos los objetos que requieren limpieza antes de que la aplicación se cierre. Dado que no se puede llamar directamente a Finalizar y no se puede garantizar que el recolector de elementos no utilizados llame a todos los finalizadores antes de salir, debe usar Dispose o DisposeAsync para asegurarse de que se liberan los recursos.

Uso de finalizadores para liberar recursos

En general, C# no requiere tanta administración de memoria por parte del desarrollador como los lenguajes que no están diseñados para un runtime con recolección de elementos no utilizados. Esto es debido a que el recolector de elementos no utilizados de .NET administra implícitamente la asignación y liberación de memoria para los objetos. En cambio, cuando la aplicación encapsule recursos no administrados, como ventanas, archivos y conexiones de red, debería usar finalizadores para liberar dichos recursos. Cuando el objeto cumple los requisitos para su finalización, el recolector de elementos no utilizados ejecuta el método Finalize del objeto.

Liberación explícita de recursos

Si la aplicación usa un recurso externo costoso, también es recomendable liberar explícitamente el recurso antes de que el recolector de elementos no utilizados libere el objeto. Para liberar el recurso, implemente un método Dispose desde la interfaz IDisposable que realiza la limpieza necesaria del objeto. Esto puede mejorar considerablemente el rendimiento de la aplicación. Aun con este control explícito sobre los recursos, el finalizador se convierte en una salvaguarda para limpiar recursos si se produce un error en la llamada al método Dispose.

Para obtener más información sobre la limpieza de recursos, vea los siguientes artículos:

Ejemplo

En el siguiente ejemplo se crean tres clases que forman una cadena de herencia. La clase First es la clase base, Second se deriva de First y Third se deriva de Second. Los tres tienen finalizadores. En Main, se crea una instancia de la clase más derivada. La salida de este código depende de la implementación de .NET a la que se dirige la aplicación:

  • .NET Framework: en la salida se muestra que se llama automáticamente a los finalizadores de las tres clases cuando finaliza la aplicación, en orden, del más derivado al menos.
  • NET 5 (incluido .NET Core) o una versión posterior: no hay ninguna salida, porque esta implementación de .NET no llama a los finalizadores cuando finaliza la aplicación.
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.
*/

Especificación del lenguaje C#

Para más información, consulte la sección Finalizadores de la especificación del lenguaje C#.

Consulte también