Patrón de Dispose

Nota:

Este contenido se ha copiado con permiso de Pearson Education, Inc. de Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2ª edición. Esa edición se publicó en 2008 y el libro se ha revisado completamente en la tercera edición. Parte de la información de esta página puede estar obsoleta.

Todos los programas adquieren uno o varios recursos del sistema, como memoria, identificadores del sistema o conexiones de base de datos, durante su ejecución. Los desarrolladores deben tener cuidado al usar estos recursos del sistema, ya que deben liberarse después de que se hayan adquirido y usado.

CLR proporciona compatibilidad con la administración automática de memoria. No es necesario liberar explícitamente la memoria administrada (memoria asignada mediante el operador new de C#). El recolector de elementos no utilizados (GC) la libera automáticamente. De esta manera, los desarrolladores se libran de la tediosa y difícil tarea de liberar memoria y ha sido una de las principales razones para la productividad sin precedentes que ofrece .NET Framework.

Por desgracia, la memoria administrada es solo uno de los muchos tipos de recursos del sistema. Otros recursos deben liberarse aún explícitamente y se conocen como recursos no administrados. La recolección de elementos no utilizados no se diseñó específicamente para administrar estos recursos no administrados, lo que significa que la responsabilidad de administrar tales recursos recae en manos de los desarrolladores.

CLR aporta algo de ayuda a la hora de liberar recursos no administrados. System.Object declara un método virtual Finalize (también denominado finalizador) al que llama la recolección de elementos no utilizados antes de que este proceso reclame la memoria del objeto y se pueda invalidar para liberar recursos no administrados. Los tipos que invalidan el finalizador se conocen como tipos finalizables.

Aunque los finalizadores son eficaces en algunos escenarios de limpieza, tienen dos inconvenientes importantes:

  • Se llama al finalizador cuando la recolección de elementos no utilizados detecta que un objeto es apto para la recopilación. Esto sucede en algún período de tiempo indeterminado después de que el recurso ya no es necesario. El retraso entre cuando el desarrollador podría o desea liberar el recurso y el momento en que el finalizador libera realmente el recurso podría ser inaceptable en los programas que adquieren muchos recursos escasos (recursos que se pueden agotar fácilmente) o en casos en los que los recursos son costosos de mantener en uso (por ejemplo, búferes de memoria grandes no administrados).

  • Cuando CLR necesita llamar a un finalizador, debe posponer la recopilación de la memoria del objeto hasta la siguiente ronda de recolección de elementos no utilizados (los finalizadores se ejecutan entre recopilaciones). Esto significa que la memoria del objeto (y todos los objetos a los que hace referencia) no se liberarán durante un período de tiempo más largo.

Por lo tanto, confiar exclusivamente en los finalizadores podría no ser adecuado en muchos escenarios cuando es importante reclamar recursos no administrados lo más rápido posible, cuando se trabaja con recursos escasos o en escenarios de gran rendimiento en los que la sobrecarga agregada de la recolección de elementos no utilizados de finalización es inaceptable.

Framework proporciona la interfaz System.IDisposable que se debe implementar para proporcionar al desarrollador una manera manual de liberar recursos no administrados en cuanto no sean necesarios. También proporciona el método GC.SuppressFinalize que puede indicar a la recolección de elementos no utilizados que un objeto se eliminó manualmente y que ya no es necesario finalizar, en cuyo caso la memoria del objeto se puede reclamar más pronto. Los tipos que implementan la interfaz IDisposable se conocen como tipos descartables.

El patrón de eliminación está diseñado para estandarizar el uso y la implementación de finalizadores y la IDisposable interfaz.

La motivación principal del patrón es reducir la complejidad de la implementación de los métodos Finalize y Dispose. La complejidad se deriva del hecho de que los métodos comparten algunas rutas de acceso de código pero no todas (las diferencias se describen más adelante en el capítulo). Además, existen razones históricas para algunos elementos del patrón relacionados con la evolución de la compatibilidad lingüística con la administración determinista de recursos.

✓ IMPLEMENTE el patrón de eliminación básico en tipos que contengan instancias de tipos descartables. Consulte la sección Patrón de eliminación básico para más información sobre el patrón básico.

Si un tipo es responsable de la duración de otros objetos descartables, los desarrolladores también necesitan una manera de eliminarlos. El uso del método Dispose del contenedor es una manera cómoda de hacerlo posible.

✓ IMPLEMENTE el patrón de eliminación básico y proporcione un finalizador sobre los tipos que contienen recursos que deben liberarse explícitamente y que no tienen finalizadores.

Por ejemplo, el patrón debe implementarse en tipos que almacenen búferes de memoria no administrados. En la sección Tipos finalizables se describen las directrices relacionadas con la implementación de finalizadores.

✓ CONSIDERE la posibilidad de implementar el patrón de eliminación básico en las clases que por sí mismas no contengan recursos no administrados u objetos descartables, pero que es probable que tengan subtipos que sí los contengan.

Un buen ejemplo de esto es la clase System.IO.Stream. Aunque se trata de una clase base abstracta que no contiene ningún recurso, la mayoría de sus subclases los contienen y, debido a esto, implementa este patrón.

Patrón de eliminación básico

La implementación básica del patrón supone implementar la interfaz System.IDisposable y declarar el método Dispose(bool) que implementa toda la lógica de limpieza de recursos que se va a compartir entre el método Dispose y el finalizador opcional.

En el ejemplo siguiente se muestra una implementación sencilla del patrón básico:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

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

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

El parámetro booleano disposing indica si el método se invocó desde la implementación IDisposable.Dispose o desde el finalizador. La implementación Dispose(bool) debe comprobar el parámetro antes de acceder a otros objetos de referencia (por ejemplo, el campo de recurso del ejemplo anterior). Solo se debe acceder a estos objetos cuando se llama al método desde la implementación de IDisposable.Dispose (cuando el parámetro disposing es igual a true). Si el método se invoca desde el finalizador (disposing es false), no se debería acceder a otros objetos. El motivo es que los objetos se finalizan en un orden imprevisible, por lo que ellos, o cualquiera de sus dependencias, podrían haberse finalizado ya.

Además, esta sección se aplica a las clases con una base que aún no implementa el patrón de eliminación. Si va a heredar de una clase que ya implementa el patrón, simplemente invalide el método Dispose(bool) para proporcionar lógica adicional de limpieza de recursos.

✓ DECLARE un método protected virtual void Dispose(bool disposing) para centralizar toda la lógica relacionada con la liberación de recursos no administrados.

Toda la limpieza de recursos debe producirse en este método. Se llama al método desde el finalizador y el método IDisposable.Dispose. El parámetro será false si se invoca desde dentro de un finalizador. Debe usarse para asegurarse de que cualquier código que se ejecute durante la finalización no tenga acceso a otros objetos finalizables. En la sección siguiente se examina la implementación de finalizadores de forma más detallada.

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ IMPLEMENTE la interfaz IDisposable con la simple llamada a Dispose(true) seguida de GC.SuppressFinalize(this).

La llamada a SuppressFinalize solo debe producirse si Dispose(true) se ejecuta correctamente.

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

X NO convierta en virtual el método sin parámetros Dispose.

El método Dispose(bool) es el que debe invalidarse por parte de las subclases.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X NO declare ninguna sobrecarga del método Dispose que no sea Dispose() y Dispose(bool).

Dispose se debe considerar una palabra reservada para ayudar a codificar este patrón y evitar confusiones entre los implementadores, los usuarios y los compiladores. Algunos lenguajes pueden optar por implementar automáticamente este patrón en determinados tipos.

✓ PERMITE llamar al método Dispose(bool) más de una vez. El método podría optar por no hacer nada después de la primera llamada.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X EVITE iniciar una excepción en Dispose(bool) excepto en situaciones críticas en las que el proceso contenedor se ha dañado (pérdidas, estado compartido incoherente, etc.).

Los usuarios esperan que una llamada a Dispose no genere una excepción.

Si existe la posibilidad de que Dispose genere una excepción, la lógica de limpieza del bloque finally no se ejecutará. Para solucionar este problema, el usuario tendría que encapsular todas las llamadas a Dispose (dentro del bloque finally!) en un bloque try, lo que conduce a controladores de limpieza muy complejos. Si va a ejecutar un método Dispose(bool disposing), no inicie nunca una excepción si la eliminación es false. Si lo hace, finalizará el proceso si se ejecuta dentro de un contexto de finalizador.

✓ INICIE una excepción ObjectDisposedException desde cualquier miembro que no se pueda usar después de que el objeto se haya eliminado.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ CONSIDERE la posibilidad de proporcionar el método Close(), además de Dispose(), si "close" es terminología estándar en el área.

Al hacerlo, es importante que la implementación Close sea idéntica a Dispose y considere la posibilidad de implementar el método IDisposable.Dispose explícitamente.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Tipos finalizables

Los tipos finalizables son tipos que invalidan el finalizador y proporcionan la ruta de acceso al código de finalización en el método Dispose(bool) para ampliar el patrón de eliminación básico.

Los finalizadores son notoriamente difíciles de implementar correctamente, principalmente porque no se pueden realizar ciertas suposiciones (normalmente válidas) sobre el estado del sistema durante su ejecución. Se deben tener en cuenta detenidamente las siguientes directrices.

Tenga en cuenta que algunas de las directrices se aplican no solo al método Finalize, sino a cualquier código llamado desde un finalizador. En el caso del patrón de eliminación básico definido anteriormente, esto significa que la lógica se ejecuta dentro de Dispose(bool disposing) cuando el parámetro disposing es false.

Si la clase base ya es finalizable e implementa el patrón de eliminación básico, no debe invalidar de nuevo Finalize. En su lugar, solo debe invalidar el método Dispose(bool) para proporcionar lógica de limpieza de recursos adicional.

En el código siguiente se muestra un ejemplo de un tipo finalizable:

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

X EVITE convertir en finalizables los tipos.

Considere cuidadosamente cualquier caso en el que piense que se necesita un finalizador. Hay un costo real asociado a los casos con finalizadores, tanto desde el punto de vista del rendimiento como de la complejidad del código. Es mejor usar contenedores de recursos como SafeHandle para encapsular recursos no administrados siempre que sea posible, en cuyo caso un finalizador resulta innecesario porque el contenedor es responsable de la limpieza de sus propios recursos.

X NO convierta en finalizables los tipos de valor.

Solo CLR finaliza realmente los tipos de referencia y, por tanto, cualquier intento de colocar un finalizador en un tipo de valor se omitirá. Los compiladores de C# y C++ aplican esta regla.

✓ CONVIERTA un tipo en finalizable si el tipo es responsable de liberar un recurso no administrado que no tiene su propio finalizador.

Al implementar el finalizador, basta con llamar a Dispose(false) y colocar toda la lógica de limpieza de recursos dentro del método Dispose(bool disposing).

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ IMPLEMENTE el patrón de eliminación básico en cada tipo finalizable.

De esta forma los usuarios del tipo tendrán un medio para realizar explícitamente la limpieza determinista de esos mismos recursos de los que el finalizador es responsable.

X NO acceda a objetos finalizables en la ruta de acceso al código del finalizador, ya que existe un riesgo significativo de que ya se hayan finalizado.

Por ejemplo, un objeto finalizable A que tiene una referencia a otro objeto finalizable B no puede usar B de forma confiable en el finalizador de A, o viceversa. Se llama a los finalizadores en orden aleatorio (a falta de una garantía de ordenación débil para la finalización crítica).

Además, tenga en cuenta que los objetos almacenados en variables estáticas se recopilarán en determinados puntos durante la descarga de un dominio de aplicación o al salir del proceso. El acceso a una variable estática que hace referencia a un objeto finalizable (o la llamada a un método estático que podría usar valores almacenados en variables estáticas) podría no ser seguro si Environment.HasShutdownStarted devuelve true.

✓ HAGA que su método Finalize está protegido.

Los desarrolladores de C#, C++ y VB.NET no tienen que preocuparse por esto, ya que los compiladores ayudan a aplicar esta directriz.

X NO permita que las excepciones escapen de la lógica del finalizador, excepto los errores críticos del sistema.

Si se produce una excepción desde un finalizador, CLR apagará todo el proceso (a partir de la versión 2.0 de .NET Framework), lo que impide que otros finalizadores se ejecuten y que los recursos se liberen de forma controlada.

✓ CONSIDERE la posibilidad de crear y usar un objeto finalizable crítico (un tipo con una jerarquía de tipos que contiene CriticalFinalizerObject) en situaciones en las que un finalizador debe ejecutarse absolutamente incluso ante descargas forzadas del dominio de aplicación y anulaciones de subprocesos.

Portions © 2005, 2009 Microsoft Corporation. Todos los derechos reservados.

Material reimpreso con el consentimiento de Pearson Education, Inc. y extraído de Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition (Instrucciones de diseño de .NET Framework: convenciones, expresiones y patrones para bibliotecas .NET reutilizables, 2.ª edición), de Krzysztof Cwalina y Brad Abrams, publicado el 22 de octubre de 2008 por Addison-Wesley Professional como parte de la serie Microsoft Windows Development.

Vea también