Implementación de un método Dispose

El método Dispose se implementa principalmente para liberar 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 eliminación se usa con los objetos que implementan la interfaz IDisposable. Este patrón es común al interactuar con identificadores de archivo y canalización, identificadores de registro, identificadores de espera o punteros a bloques de memoria sin administrar, ya 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 su 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. SafeHandle 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 noSafeHandle 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 objetos SafeHandle.

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

Clase Recursos que contiene
SafeFileHandle
SafeMemoryMappedFileHandle
SafePipeHandle
Archivos, archivos asignados en memoria y canalizaciones
SafeMemoryMappedViewHandle Vistas de memoria
SafeNCryptKeyHandle
SafeNCryptProviderHandle
SafeNCryptSecretHandle
Construcciones de criptografía
SafeRegistryHandle Claves del Registro
SafeWaitHandle Identificadores de espera

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).

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. 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 el siguiente 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

  • Si la clase tiene un campo o una propiedad IDisposable, pero no los posee (es decir, la clase no crea el objeto), la clase no necesita implementar IDisposable.
  • En algunos casos, es posible que quiera realizar una comprobación de valores null en un finalizador (que incluye el método Dispose(false) invocado por un finalizador). Principalmente si no sabe con seguridad 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 eliminación. 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 se muestra un ejemplo 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;

public 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);
        GC.SuppressFinalize(this);
    }

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

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

Public Class BaseClassWithSafeHandle
    Implements IDisposable

    ' To detect redundant calls
    Private _disposedValue As Boolean

    ' Instantiate a SafeHandle instance.
    Private _safeHandle 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(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                _safeHandle?.Dispose()
                _safeHandle = Nothing
            End If

            _disposedValue = True
        End If
    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;

public 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;
        }
    }
}
Public Class BaseClassWithFinalizer
    Implements IDisposable

    ' To detect redundant calls
    Private _disposedValue As Boolean

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

    ' 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(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                ' TODO: dispose managed state (managed objects)
            End If

            ' TODO free unmanaged resources (unmanaged objects) And override finalizer
            ' TODO: set large fields to null
            _disposedValue = True
        End If
    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;

public 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();
                _safeHandle = null;
            }

            _disposedValue = true;
        }

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

Public Class DerivedClassWithSafeHandle
    Inherits BaseClassWithSafeHandle

    ' To detect redundant calls
    Private _disposedValue As Boolean

    ' Instantiate a SafeHandle instance.
    Private _safeHandle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True)

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                _safeHandle?.Dispose()
                _safeHandle = Nothing
            End If

            _disposedValue = True
        End If

        ' 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:

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

    ~DerivedClassWithFinalizer() => 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);
    }
}
Public Class DerivedClassWithFinalizer
    Inherits BaseClassWithFinalizer

    ' To detect redundant calls
    Private _disposedValue As Boolean

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

    ' Protected implementation of Dispose pattern.
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If Not _disposedValue Then

            If disposing Then
                ' TODO: dispose managed state (managed objects).
            End If

            ' TODO free unmanaged resources (unmanaged objects) And override a finalizer below.
            ' TODO: set large fields to null.
            _disposedValue = True
        End If

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

Consulte también