Compartir vía


Patrón de eliminación

Nota:

Este contenido se reimprime con permiso de Pearson Education, Inc. de Directrices de diseño de frameworks: Convenciones, expresiones y patrones para bibliotecas reutilizables de .NET, 2ª edición. Esa edición fue publicada en 2008, y el libro ha sido totalmente revisado 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 la memoria, los identificadores del sistema o las conexiones de base de datos, durante el transcurso de 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 newde C#). El recolector de basura lo libera automáticamente. Esto libera a los desarrolladores de la tarea tediosa y difícil de liberar memoria y ha sido una de las principales razones para la productividad sin precedentes que ofrece .NET Framework.

Desafortunadamente, la memoria administrada es solo uno de los muchos tipos de recursos del sistema. Los recursos que no sean la memoria administrada todavía deben liberarse explícitamente y se conocen como recursos no administrados. El GC no se diseñó específicamente para administrar estos recursos no administrados, lo que significa que la responsabilidad de administrar recursos no administrados reside en manos de los desarrolladores.

CLR proporciona ayuda para liberar recursos no administrados. System.Object declara un método Finalize virtual (también denominado finalizador) al que llama el GC antes de que el GC recupere la memoria del objeto y se pueda invalidar para liberar recursos no administrados. Los tipos que invalidan el finalizador se denominan tipos finalizables.

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

  • Se llama al finalizador cuando el GC detecta que un objeto es apto para la colección. Esto sucede en algún período de tiempo indeterminado después de que el recurso ya no sea necesario. El retraso entre cuando el desarrollador podría o desea liberar el recurso y el momento en que el finalizador realmente libera 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 mantenerse en uso (por ejemplo, búferes de memoria no administrados grandes).

  • Cuando el CLR necesita llamar a un finalizador, debe posponer la recopilación de la memoria del objeto hasta la siguiente ronda de recolección de basura (los finalizadores se ejecutan entre colecciones). 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 antes posible, cuando se trabaja con recursos escasos o en escenarios de gran rendimiento en los que la sobrecarga de GC agregada de la finalización es inaceptable.

Framework proporciona la System.IDisposable interfaz 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 GC.SuppressFinalize método que permite indicar al GC que un objeto se ha eliminado manualmente y que no es necesario finalizar, por lo que se puede reclamar la memoria del objeto antes. Los tipos que implementan la IDisposable interfaz se conocen como tipos descartables.

El patrón Dispose 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 Finalize métodos y Dispose . La complejidad se deriva del hecho de que los métodos comparten algunas, pero no todas las rutas de acceso de código (las diferencias se describen más adelante en el capítulo). Además, existen razones históricas por algunos elementos del patrón relacionados con la evolución de la compatibilidad del lenguaje con la administración de recursos determinista.

✓ IMPLEMENTE el patrón de eliminación básico en tipos que contienen instancias de tipos desechables. Consulte la sección Patrón básico de Dispose para obtener 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 del Dispose contenedor es una manera cómoda de hacerlo posible.

✓ Implemente el patrón Basic Dispose 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 almacenan 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 Basic Dispose en las clases que no contienen recursos no administrados o objetos descartables, pero que es probable que tengan subtipos que sí.

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

Patrón básico de Dispose

La implementación básica del patrón implica implementar la System.IDisposable interfaz y declarar el Dispose(bool) método que implementa toda la lógica de limpieza de recursos que se va a compartir entre el Dispose método 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 disposing booleano indica si el método se invocó desde la IDisposable.Dispose implementación o desde el finalizador. La Dispose(bool) implementación 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 tener acceso a estos objetos cuando se llama al método desde la IDisposable.Dispose implementación (cuando el disposing parámetro es igual a true). Si se invoca el método desde el finalizador (disposing es false), no se debe tener acceso a otros objetos. La razón es que los objetos se finalizan en un orden imprevisible y, por lo tanto, es posible que ellos o cualquiera de sus dependencias ya se hayan finalizado.

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

✓ Declarar un protected virtual void Dispose(bool disposing) método 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. El método se llama tanto desde el finalizador como desde 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. Los detalles de la implementación de finalizadores se describen en la sección siguiente.

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

✓ DO implemente la IDisposable interfaz simplemente llamando Dispose(true) a seguido 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 hace que el método sin parámetros Dispose sea virtual.

El Dispose(bool) método es el que se debe reemplazar por 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 sean Dispose() y Dispose(bool).

Dispose debe considerarse 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.

✓ DO permite Dispose(bool) llamar al método 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 lanzar una excepción desde dentro de Dispose(bool) excepto en situaciones críticas en las que el proceso contenedor se ha corrompido (pérdidas, estado compartido incoherente, etc.).

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

Si Dispose podría generar una excepción, no se ejecutará ninguna lógica de limpieza de bloques adicionales. Para solucionar esto, el usuario tendría que envolver todas las llamadas a Dispose (dentro del bloque finally) en un bloque try, lo que conduce a manejadores de limpieza muy complejos. Si ejecuta un método Dispose(bool disposing), nunca lance una excepción si disposición es false. Si lo hace, finalizará el proceso si se ejecuta dentro de un contexto de finalizador.

✓ Lanza un ObjectDisposedException en cualquier miembro que no se pueda usar después de que el objeto se haya desechado.

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 la terminología estándar en el área.

Al hacerlo, es importante que haga que la Close implementación sea idéntica a Dispose y considere la posibilidad de implementar el IDisposable.Dispose método 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 amplían el Patrón Básico de Eliminación reemplazando el finalizador y proporcionando el camino para el código de finalización en el método Dispose(bool).

Los finalizadores son notoriamente difíciles de implementar correctamente, principalmente porque no se pueden hacer 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 Finalize método , sino a cualquier código llamado desde un finalizador. En el caso del patrón Basic Dispose definido anteriormente, esto significa la lógica que 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 Basic Dispose, no debe invalidar Finalize de nuevo. En su lugar, solo debe sobrescribir 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 los tipos en finalizables.

Considere cuidadosamente cualquier caso en el que piense que se necesita un finalizador. Hay un costo real asociado a instancias de finalizadores, desde el punto de vista de la complejidad del código y del rendimiento. Prefiere usar contenedores de recursos como SafeHandle para encapsular recursos no administrados siempre que sea posible, en cuyo caso un finalizador se vuelve innecesario porque el contenedor es responsable de su propia limpieza de recursos.

X NO PERMITAS que los tipos de valor sean finalizables.

Solo los tipos de referencia son realmente finalizados por el CLR, y por tanto, cualquier intento de aplicar un finalizador a un tipo de valor no será procesado. Los compiladores de C# y C++ aplican esta regla.

✓ HAGA que un tipo sea finalizable si el tipo es responsable de liberar un recurso no administrado que no tenga su propio finalizador.

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

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

✓ IMPLEMENTE el patrón de eliminación básico en todos los tipos finalizables.

Esto proporciona a los usuarios del tipo un medio para realizar explícitamente la limpieza determinista de esos mismos recursos para los que el finalizador es responsable.

X NO accede a ningún objeto finalizable en la ruta de acceso del 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. Los finalizadores se llaman en un orden aleatorio (a menos 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. Acceder a una variable estática que hace referencia a un objeto finalizable (o llamar 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 el Finalize método está protegido.

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

X NO permite 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 los recursos se liberen de forma controlada.

✓ CONSIDERE crear y usar un objeto finalizable crítico (un tipo con una jerarquía de tipos que contiene CriticalFinalizerObject) para situaciones en las que un finalizador absolutamente debe ejecutarse incluso frente a cargas forzadas del dominio de aplicación e interrupciones de subprocesos.

© Partes 2005, 2009 de Microsoft Corporation. Todos los derechos reservados.

Reimpreso con permiso de Pearson Education, Inc. de Framework Design Guidelines: Convenciones, Idiomas y Patrones para Bibliotecas .NET Reusables, 2ª Edición por Krzysztof Cwalina y Brad Abrams, publicado el 22 de octubre de 2008 por Addison-Wesley Professional como parte de la Serie Desarrollo de Microsoft Windows.

Consulte también