Implementación de un método Dispose

La implementación del método Dispose sirve principalmente para publicar recursos no administrados. Al trabajar con miembros de instancia que son implementaciones de IDisposable, es habitual hacer llamadas de Dispose en cascada. Hay otras razones para implementar Dispose, por ejemplo, para liberar memoria que se ha asignado, quitar un elemento que se ha agregado a una colección o señalar la liberación de un bloqueo adquirido.

El recolector de elementos no utilizados de .NET no asigna ni libera memoria no administrada. El modelo para desechar un objeto, lo que se conoce como patrón de Dispose, sirve para imponer orden sobre la duración de un objeto. El patrón de Dispose se utiliza solo con los objetos que implementan la inferfaz IDisposable, y es común al interactuar con identificadores de archivo y de canalización, identificadores de registro, identificadores de espera o punteros a bloques de memoria sin administrar. Esto se debe a que el recolector de elementos no utilizados no puede reclamar objetos no administrados.

Para asegurarse de que los recursos se limpien siempre correctamente, un método Dispose debe ser idempotente, de manera que sea invocable varias veces sin que se produzca una excepción. Además, las siguientes invocaciones de Dispose no deben hacer nada.

El ejemplo de código proporcionado para el método GC.KeepAlive muestra cómo la recolección de elementos no utilizados puede hacer que un finalizador se ejecute mientras una referencia no administrada al objeto o a sus miembros todavía está en uso. Usar GC.KeepAlive tiene sentido para hacer que el objeto no sea válido para la recolección de elementos no utilizados desde el principio de la rutina actual y hasta el momento en que se llamó a este método.

Sugerencia

Con respecto a la inserción de dependencias, al registrar servicios en IServiceCollection, la duración del servicio se administra implícitamente en tu nombre. El elemento IServiceProvider y el elemento IHost correspondiente orquestan la limpieza de recursos. En concreto, las implementaciones de IDisposable y IAsyncDisposable se eliminan correctamente al final de su duración especificada.

Para más información, vea Inserción de dependencias en .NET.

Identificadores seguros

La escritura de código para el finalizador de un objeto es una tarea compleja que puede producir problemas si no se realiza correctamente. Por tanto, se recomienda construir objetos System.Runtime.InteropServices.SafeHandle en lugar de implementar un finalizador.

Un System.Runtime.InteropServices.SafeHandle es un tipo administrado abstracto que contiene un System.IntPtr que identifica un recurso no administrado. En Windows, puede identificar un identificador y, en UNIX, un descriptor de archivo. Proporciona toda la lógica necesaria para asegurarse de que este recurso se libera una vez y solo una vez, cuando se elimina SafeHandle o cuando se quitan todas las referencias a SafeHandle y se finaliza la instancia de SafeHandle.

System.Runtime.InteropServices.SafeHandle es una clase base abstracta. Las clases derivadas proporcionan instancias específicas para diferentes tipos de identificadores. Estas clases derivadas validan qué valores de System.IntPtr se consideran no válidos y cómo liberar realmente el identificador. Por ejemplo, SafeFileHandle se deriva de SafeHandle para ajustar IntPtrs que identifican los identificadores o descriptores de archivos abiertos e invalida su método SafeHandle.ReleaseHandle() para cerrarlo (a través de la función close en Unix o la función CloseHandle en Windows). La mayoría de las API de las bibliotecas de .NET que crean un recurso no administrado lo encapsularán en SafeHandle y devolverán ese SafeHandle según sea necesario, en lugar de volver a entregar el puntero básico. En situaciones en las que interactúe con un componente no administrado y obtenga IntPtr para un recurso no administrado, puede crear su propio tipo de SafeHandle para ajustarlo. Como resultado, algunos tipos no SafeHandle necesitan implementar finalizadores; la mayoría de las implementaciones de patrón descartable solo terminan con el ajuste de otros recursos administrados, algunos de los cuales pueden ser SafeHandle.

Las clases derivadas siguientes en el espacio de nombres Microsoft.Win32.SafeHandles proporcionan controladores seguros:

Dispose() y Dispose (booleano)

La interfaz IDisposable requiere la implementación de un único método sin parámetros, Dispose. Además, cualquier clase no sellada debe tener un método de sobrecarga Dispose(bool) adicional.

Las firmas de método son:

  • public no virtual (NotOverridable en Visual Basic) (IDisposable.Dispose implementación).
  • protected virtual (Overridable en Visual Basic) Dispose(bool).

Método Dispose()

Dado que un consumidor del tipo llama a este métodoDisposepublic, no virtual (NotOverridable en Visual Basic) y sin parámetros cuando ya no se necesita en liberar recursos no administrados, realizar limpiezas generales e indicar que el finalizador, si existe, no tiene que ejecutarse. La liberación de la memoria real asociada a un objeto administrado es siempre una tarea que corresponde al recolector de elementos no utilizados. Debido a esto, se realiza una implementación estándar:

public void Dispose()
{
    // Dispose of unmanaged resources.
    Dispose(true);
    // Suppress finalization.
    GC.SuppressFinalize(this);
}
Public Sub Dispose() _
    Implements IDisposable.Dispose
    ' Dispose of unmanaged resources.
    Dispose(True)
    ' Suppress finalization.
    GC.SuppressFinalize(Me)
End Sub

El método Dispose limpia todos los objetos, por lo que el recolector de elementos no utilizados no necesita llamar a la invalidación Object.Finalize de los objetos. Por consiguiente, la llamada al método SuppressFinalize evita que el recolector de elementos no utilizados ejecute el finalizador. Si el tipo no tiene ningún finalizador, la llamada a GC.SuppressFinalize no tiene ningún efecto. Tenga en cuenta que la limpieza real se realiza mediante la sobrecarga del método Dispose(bool).

Sobrecarga del método Dispose (bool)

En la sobrecarga, el parámetro disposing es un valor Boolean que indica si la llamada al método procede de un método Dispose (su valor es true) o de un finalizador (su valor es false).

protected virtual void Dispose(bool disposing)
{
    if (_disposed)
    {
        return;
    }

    if (disposing)
    {
        // TODO: dispose managed state (managed objects).
    }

    // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
    // TODO: set large fields to null.

    _disposed = true;
}
Protected Overridable Sub Dispose(disposing As Boolean)
     If disposed Then Exit Sub	

     ' A block that frees unmanaged resources.
     
     If disposing Then
         ' Deterministic call…
         ' A conditional block that frees managed resources.    	
     End If
     
     disposed = True
End Sub

Importante

El parámetro disposing debe ser false cuando se llama desde un finalizador y true cuando se llama desde el método IDisposable.Dispose. En otras palabras, es true cuando se llama de forma determinista y false cuando se llama de forma no determinista.

El cuerpo del método consta de tres bloques de código:

  • Bloque para la devolución condicional si el objeto ya se ha eliminado.

  • Un bloque que libera los recursos no administrados. Este bloque se ejecuta independientemente del valor del parámetro disposing.

  • Un bloque condicional que libera los recursos administrados. Este bloque se ejecuta si el valor de disposing es true. Estos son algunos de los recursos administrados que se liberan:

    • Objetos administrados que implementan IDisposable. El bloque condicional se puede utilizar para llamar a la implementación Dispose (eliminación en cascada). Si ha utilizado una clase derivada de System.Runtime.InteropServices.SafeHandle para ajustar el recurso no administrado, debe llamar aquí a la implementación SafeHandle.Dispose().

    • Objetos administrados que consumen gran cantidad de memoria o recursos insuficientes. Asigne referencias de objetos administrados grandes a null para aumentar la probabilidad de que no se pueda acceder a ellos. De este modo, se liberan más rápido que si se recuperaran de forma no determinista.

Si la llamada al método procede de un finalizador, solo se debe ejecutar el código que libera los recursos no administrados. El implementador es responsable de garantizar que la ruta de acceso falsa no interactúe con los objetos administrados que se pueden haber dispuesto. Esto es importante porque el orden en el que el recolector de elementos no utilizados dispone los objetos administrados durante la finalización no es determinista.

Llamadas de eliminación en cascada

Si la clase posee un campo o una propiedad y su tipo implementa IDisposable, la propia clase contenedora también debe implementar IDisposable. Una clase que crea instancias de una implementación de IDisposable y la almacena como un miembro de instancia, también es responsable de su limpieza. Esto ayuda a garantizar que los tipos descartables a los que se hace referencia tienen la oportunidad de realizar una limpieza determinista mediante el método Dispose. En este ejemplo, la clase es sealed (o NotInheritable en Visual Basic).

using System;

public sealed class Foo : IDisposable
{
    private readonly IDisposable _bar;

    public Foo()
    {
        _bar = new Bar();
    }

    public void Dispose() => _bar.Dispose();
}
Public NotInheritable Class Foo
    Implements IDisposable

    Private ReadOnly _bar As IDisposable

    Public Sub New()
        _bar = New Bar()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        _bar.Dispose()
    End Sub
End Class

Sugerencia

Hay casos en los que es posible que desee realizar nullla comprobación de un finalizador (que incluye el Dispose(false) método invocado por un finalizador), una de las razones principales es si no está seguro de si la instancia se ha inicializado completamente (por ejemplo, se puede producir una excepción en un constructor).

Implementación del patrón Dispose

Todas las clases no selladas o (clases de Visual Basic no modificadas como NotInheritable) deben considerarse una clase base potencial, ya que se podrían heredar. Cuando se implementa el patrón de Dispose para cualquier clase base potencial, debe proporcionar lo siguiente:

  • Una implementación Dispose que llame al método Dispose(bool).
  • Un método Dispose(bool) que realiza la tarea real de limpieza.
  • Una clase derivada de SafeHandle que contiene el recurso no administrado (recomendado), o una invalidación del método Object.Finalize. La clase SafeHandle proporciona un finalizador, por lo que no tiene que escribir uno personalmente.

Importante

Es posible que una clase base solo haga referencia a objetos administrados e implemente el patrón de Dispose. En estos casos, un finalizador no es necesario. Un finalizador solo es necesario si se hace referencia directamente a los recursos no administrados.

A continuación hay un ejemplo del patrón general para implementar el patrón de Dispose para una clase base que utiliza un controlador seguro.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

class BaseClassWithSafeHandle : IDisposable
{
    // To detect redundant calls
    private bool _disposedValue;

    // Instantiate a SafeHandle instance.
    private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true);

    // Public implementation of Dispose pattern callable by consumers.
    public void Dispose() => Dispose(true);

    // Protected implementation of Dispose pattern.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                _safeHandle.Dispose();
            }

            _disposedValue = true;
        }
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.InteropServices

Class BaseClassWithSafeHandle : Implements IDisposable
    ' Flag: Has Dispose already been called?
    Dim disposed As Boolean = False
    ' Instantiate a SafeHandle instance.
    Dim handle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True)

    ' Public implementation of Dispose pattern callable by consumers.
    Public Sub Dispose() _
               Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ' Protected implementation of Dispose pattern.
    Protected Overridable Sub Dispose(disposing As Boolean)
        If disposed Then Return

        If disposing Then
            handle.Dispose()
        End If

        disposed = True
    End Sub
End Class

Nota

El ejemplo anterior utiliza un objeto SafeFileHandle para ilustrar el patrón; cualquier objeto derivado de SafeHandle podría usarse en su lugar. Tenga en cuenta que el ejemplo no crea una instancia de su objeto SafeFileHandle correctamente.

A continuación se muestra el patrón general para implementar el patrón de Dispose para una clase base que invalide a Object.Finalize.

using System;

class BaseClassWithFinalizer : IDisposable
{
    // To detect redundant calls
    private bool _disposedValue;

    ~BaseClassWithFinalizer() => Dispose(false);

    // Public implementation of Dispose pattern callable by consumers.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // Protected implementation of Dispose pattern.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _disposedValue = true;
        }
    }
}
Class BaseClassWithFinalizer : Implements IDisposable
    ' Flag: Has Dispose already been called?
    Dim disposed As Boolean = False

    ' Public implementation of Dispose pattern callable by consumers.
    Public Sub Dispose() _
               Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ' Protected implementation of Dispose pattern.
    Protected Overridable Sub Dispose(disposing As Boolean)
        If disposed Then Return

        If disposing Then
            ' Dispose managed objects that implement IDisposable.
            ' Assign null to managed objects that consume large amounts of memory or consume scarce resources.
        End If

        ' Free any unmanaged objects here.
        '
        disposed = True
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
    End Sub
End Class

Sugerencia

En C#, se implementa una finalización proporcionando un finalizador, no invalidando Object.Finalize. En Visual Basic, se crea un finalizador con Protected Overrides Sub Finalize().

Implementación del patrón de Dispose para una clase derivada

Una clase derivada de una clase que implemente la interfaz IDisposable no debe implementar IDisposable, porque la implementación de la clase base de IDisposable.Dispose la heredan sus clases derivadas. En su lugar, para limpiar una clase derivada, debe proporcionar los siguientes elementos:

  • Un método protected override void Dispose(bool) que invalide el método de la clase base y realice la limpieza real de la clase derivada. Este método también debe llamar al base.Dispose(bool) método (MyBase.Dispose(bool) en Visual Basic) pasando el estado de eliminación (bool disposing parámetro) como argumento.
  • Una clase derivada de SafeHandle que contiene el recurso no administrado (recomendado), o una invalidación del método Object.Finalize. La clase SafeHandle proporciona un finalizador que evita que tenga que codificar uno. Si proporciona un finalizador, debe llamar a la sobrecarga de Dispose(bool) con un argumento false.

A continuación hay un ejemplo del patrón general para implementar el patrón de Dispose para una clase derivado que utiliza un controlador seguro:

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

class DerivedClassWithSafeHandle : BaseClassWithSafeHandle
{
    // To detect redundant calls
    private bool _disposedValue;

    // Instantiate a SafeHandle instance.
    private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true);

    // Protected implementation of Dispose pattern.
    protected override void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                _safeHandle.Dispose();
            }

            _disposedValue = true;
        }

        // Call base class implementation.
        base.Dispose(disposing);
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.Runtime.InteropServices

Class DerivedClassWithSafeHandle : Inherits BaseClassWithSafeHandle
    ' Flag: Has Dispose already been called?
    Dim disposed As Boolean = False
    ' Instantiate a SafeHandle instance.
    Dim handle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True)

    ' Protected implementation of Dispose pattern.
    Protected Overrides Sub Dispose(disposing As Boolean)
        If disposed Then Return

        If disposing Then
            handle.Dispose()
            ' Free any other managed objects here.
            '
        End If

        ' Free any unmanaged objects here.
        '
        disposed = True

        ' Call base class implementation.
        MyBase.Dispose(disposing)
    End Sub
End Class

Nota

El ejemplo anterior utiliza un objeto SafeFileHandle para ilustrar el patrón; cualquier objeto derivado de SafeHandle podría usarse en su lugar. Tenga en cuenta que el ejemplo no crea una instancia de su objeto SafeFileHandle correctamente.

A continuación se muestra el patrón general para implementar el patrón de Dispose para una clase derivada que invalide a Object.Finalize:

class DerivedClassWithFinalizer : BaseClassWithFinalizer
{
    // To detect redundant calls
    private bool _disposedValue;

    ~DerivedClassWithFinalizer() => this.Dispose(false);

    // Protected implementation of Dispose pattern.
    protected override void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects).
            }

            // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
            // TODO: set large fields to null.
            _disposedValue = true;
        }

        // Call the base class implementation.
        base.Dispose(disposing);
    }
}
Class DerivedClassWithFinalizer : Inherits BaseClassWithFinalizer
    ' Flag: Has Dispose already been called?
    Dim disposed As Boolean = False

    ' Protected implementation of Dispose pattern.
    Protected Overrides Sub Dispose(disposing As Boolean)
        If disposed Then Return

        If disposing Then
            ' Dispose managed objects that implement IDisposable.
            ' Assign null to managed objects that consume large amounts of memory or consume scarce resources.
        End If

        ' Free any unmanaged objects here.
        '
        disposed = True

        ' Call the base class implementation.
        MyBase.Dispose(disposing)
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
    End Sub
End Class

Consulte también