Examen de los finalizadores de clases

Completado

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

Algunas cosas que se deben tener en cuenta al usar finalizadores:

  • Los finalizadores no se pueden definir en tipos de struct. 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 toma modificadores ni tiene parámetros.

Sintaxis de un finalizador

En C#, un finalizador se define mediante una tilde (~) seguida del nombre de clase. No toma ningún parámetro y no se puede llamar explícitamente.


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

Un finalizador también se puede implementar como 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.");
}

Relación entre la recolección de elementos no utilizados y los finalizadores

El recolector de elementos no utilizados de .NET administra automáticamente la asignación y liberación de memoria para los objetos administrados. Cuando ya no se hace referencia a un objeto, el GC lo marca para la colección y, finalmente, reclama su memoria. Si una clase tiene un finalizador, el GC llama al finalizador antes de reclamar la memoria del objeto. El finalizador permite que el objeto libere los recursos no administrados que contiene.

Normalmente, el proceso de finalización implica los pasos siguientes:

  • Cuando el GC detecta que ya no se puede acceder a un objeto con un finalizador, mueve el objeto a una cola de finalización.
  • El subproceso del finalizador ejecuta el método de finalizador.
  • Una vez que se ejecuta el finalizador, el objeto se mueve a la cola de freachable del GC, donde es apto para la recolección de elementos no utilizados en el siguiente ciclo de GC.

Ejemplo de finalizador

public class ResourceHolder
{
    // Unmanaged resource
    private IntPtr unmanagedResource;

    // Constructor
    public ResourceHolder()
    {
        // Allocate unmanaged resource
        unmanagedResource = /* allocate resource */;
    }

    // Finalizer
    ~ResourceHolder()
    {
        // Release unmanaged resource
        if (unmanagedResource != IntPtr.Zero)
        {
            // Free the resource
            /* free resource */
            unmanagedResource = IntPtr.Zero;
        }
    }
}

Implementación de un patrón dispose con un finalizador

Las interfaces como IDisposable y IAsyncDisposable se usan para liberar recursos de forma determinista. El método Dispose se llama explícitamente para liberar recursos, mientras que el finalizador actúa como una red de seguridad para asegurarse de que los recursos se liberan incluso si no se llama a Dispose.

Importante

La creación y el uso de interfaces están fuera del ámbito de este módulo. El uso de IDisposable se describe aquí para proporcionar contexto para el patrón "dispose". El entrenamiento que proporciona una introducción a las interfaces está disponible en la plataforma de Microsoft Learn.

Tenga en cuenta los siguientes puntos al usar finalizadores:

  • Los finalizadores no son deterministas, lo que significa que no se puede predecir cuándo se ejecuta el finalizador. Depende de la programación del GC. Este comportamiento puede provocar retrasos en la liberación de recursos no administrados.
  • Los finalizadores pueden afectar al rendimiento porque los objetos con finalizadores tardan más tiempo en recopilarse.
  • Se recomienda implementar la interfaz IDisposable y el método Dispose para la limpieza determinista de los recursos. El método Dispose se puede llamar explícitamente para liberar recursos y el finalizador se puede usar como red de seguridad.

Este es un ejemplo de una clase que implementa la interfaz IDisposable y un finalizador:

public class ResourceHolder : IDisposable
{
    // Unmanaged resource
    private IntPtr unmanagedResource;
    private bool disposed = false;

    // Constructor
    public ResourceHolder()
    {
        // Allocate unmanaged resource
        unmanagedResource = /* allocate resource */;
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Free other managed objects
            }

            // Free unmanaged resources
            if (unmanagedResource != IntPtr.Zero)
            {
                /* free resource */
                unmanagedResource = IntPtr.Zero;
            }

            disposed = true;
        }
    }

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

En este ejemplo:

  • Se llama al método Dispose para liberar recursos de forma determinista.
  • El finalizador llama a Dispose(false) para liberar recursos no administrados si no se llamó a Dispose.
  • GC.SuppressFinalize(this) se usa para evitar que el finalizador se ejecute si ya se ha llamado a Dispose.