Uso del método Finalizar/Disponer en C#

Resuelto ant2009 asked hace 15 años • 13 respuestas

c#2008

He estado trabajando en esto por un tiempo y todavía estoy confundido acerca del uso de los métodos de finalización y eliminación en el código. Mis preguntas están a continuación:

  1. Sé que solo necesitamos un finalizador mientras desechamos los recursos no administrados. Sin embargo, si hay recursos administrados que realizan llamadas a recursos no administrados, ¿aún sería necesario implementar un finalizador?

  2. Sin embargo, si desarrollo una clase que no utiliza ningún recurso no administrado, directa o indirectamente, ¿debería implementar IDisposablepara permitir que los clientes de esa clase utilicen la 'declaración de uso'?

    ¿Sería factible implementar IDisposable solo para permitir que los clientes de su clase usen la declaración de uso?

    using(myClass objClass = new myClass())
    {
        // Do stuff here
    }
    
  3. He desarrollado este código simple a continuación para demostrar el uso de Finalizar/eliminar:

    public class NoGateway : IDisposable
    {
        private WebClient wc = null;
    
        public NoGateway()
        {
            wc = new WebClient();
            wc.DownloadStringCompleted += wc_DownloadStringCompleted;
        }
    
    
        // Start the Async call to find if NoGateway is true or false
        public void NoGatewayStatus()
        {
            // Start the Async's download
                // Do other work here
            wc.DownloadStringAsync(new Uri(www.xxxx.xxx));
        }
    
        private void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            // Do work here
        }
    
        // Dispose of the NoGateway object
        public void Dispose()
        {
            wc.DownloadStringCompleted -= wc_DownloadStringCompleted;
            wc.Dispose();
            GC.SuppressFinalize(this);
        }
    }
    

Pregunta sobre el código fuente:

  1. Aquí no he agregado el finalizador, y normalmente el GC llamará al finalizador, y el finalizador llamará a Dispose. Como no tengo el finalizador, ¿cuándo llamo al método Dispose? ¿Es el cliente de la clase el que tiene que llamarlo?

    Entonces, mi clase en el ejemplo se llama NoGateway y el cliente podría usar y eliminar la clase de esta manera:

    using(NoGateway objNoGateway = new NoGateway())
    {
        // Do stuff here   
    }
    

    ¿Se llamará automáticamente al método Dispose cuando la ejecución llegue al final del bloque de uso, o el cliente tendrá que llamar manualmente al método dispose? es decir

    NoGateway objNoGateway = new NoGateway();
    // Do stuff with object
    objNoGateway.Dispose(); // finished with it
    
  2. Estoy usando la WebClientclase en mi NoGatewayclase. Debido a WebClientque implementa la IDisposableinterfaz, ¿significa esto que WebClientutiliza indirectamente recursos no administrados? ¿Existe una regla estricta y rápida para seguir esto? ¿Cómo sé que una clase utiliza recursos no administrados?

ant2009 avatar May 22 '09 23:05 ant2009
Aceptado

El patrón IDisposable recomendado está aquí . Al programar una clase que usa IDisposable, generalmente debes usar dos patrones:

Al implementar una clase sellada que no utiliza recursos no administrados, simplemente implementa un método Dispose como con las implementaciones de interfaz normales:

public sealed class A : IDisposable
{
    public void Dispose()
    {
        // get rid of managed resources, call Dispose on member variables...
    }
}

Al implementar una clase abierta, hágalo así:

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }   
        // get rid of unmanaged resources
    }

    // only if you use unmanaged resources directly in B
    //~B()
    //{
    //    Dispose(false);
    //}
}

Observe que no he declarado un finalizador en B; solo debe implementar un finalizador si tiene recursos reales no administrados para eliminar. El CLR trata los objetos finalizables de manera diferente a los objetos no finalizables, incluso si SuppressFinalizese llama.

Por lo tanto, no debes declarar un finalizador a menos que sea necesario, pero les brindas a los herederos de tu clase un gancho para llamar Disposee implementar un finalizador ellos mismos si usan recursos no administrados directamente:

public class C : B
{
    private IntPtr m_Handle;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }
        ReleaseHandle(m_Handle);

        base.Dispose(disposing);
    }

    ~C() {
        Dispose(false);
    }
}

Si no está utilizando recursos no administrados directamente ( SafeHandley los amigos no cuentan, ya que declaran sus propios finalizadores), no implemente un finalizador, ya que el GC trata las clases finalizables de manera diferente, incluso si luego suprime el finalizador. También tenga en cuenta que, aunque Bno tiene un finalizador, todavía llama SuppressFinalizepara tratar correctamente cualquier subclase que implemente un finalizador.

Cuando una clase implementa la interfaz IDisposable, significa que en algún lugar hay algunos recursos no administrados de los que debes deshacerte cuando hayas terminado de usar la clase. Los recursos reales están encapsulados dentro de las clases; no es necesario eliminarlos explícitamente. Simplemente llamar Dispose()o envolver la clase en un using(...) {}asegurará que los recursos no administrados se eliminen según sea necesario.

thecoop avatar May 22 '2009 16:05 thecoop

El patrón oficial a implementar IDisposablees difícil de entender. Creo que este es mejor :

public class BetterDisposableClass : IDisposable {

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

  protected virtual void CleanUpManagedResources() { 
    // ...
  }
  protected virtual void CleanUpNativeResources() {
    // ...
  }

  ~BetterDisposableClass() {
    CleanUpNativeResources();
  }

}

Una solución aún mejor es tener una regla que indique que siempre debe crear una clase contenedora para cualquier recurso no administrado que necesite manejar:

public class NativeDisposable : IDisposable {

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

  protected virtual void CleanUpNativeResource() {
    // ...
  }

  ~NativeDisposable() {
    CleanUpNativeResource();
  }

}

Con SafeHandley sus derivados, estas clases deberían ser muy raras .

El resultado para las clases desechables que no tratan directamente con recursos no administrados, incluso en presencia de herencia, es poderoso: ya no necesitan preocuparse por los recursos no administrados . Serán simples de implementar y comprender:

public class ManagedDisposable : IDisposable {

  public virtual void Dispose() {
    // dispose of managed resources
  }

}
Jordão avatar Jan 29 '2010 17:01 Jordão

Tenga en cuenta que cualquier implementación de IDisposable debe seguir el siguiente patrón (en mi humilde opinión). Desarrollé este patrón basándome en información de varios "dioses" excelentes de .NET, las pautas de diseño de .NET Framework (tenga en cuenta que MSDN no sigue esto por alguna razón). Las pautas de diseño de .NET Framework fueron escritas por Krzysztof Cwalina (arquitecto de CLR en ese momento) y Brad Abrams (creo que el gerente del programa CLR en ese momento) y Bill Wagner ([C# efectivo] y [C# más efectivo] (solo tome un busque estos en Amazon.com:

Tenga en cuenta que NUNCA debe implementar un Finalizador a menos que su clase contenga directamente (no herede) recursos NO administrados. Una vez que implementas un Finalizer en una clase, incluso si nunca se llama, se garantiza que vivirá para una colección adicional. Se coloca automáticamente en la cola de finalización (que se ejecuta en un solo subproceso). Además, una nota muy importante... ¡todo el código ejecutado dentro de un Finalizador (en caso de que necesite implementar uno) DEBE ser seguro para subprocesos Y seguro para excepciones! De lo contrario, sucederán cosas MALAS... (es decir, un comportamiento indeterminado y, en el caso de una excepción, un fallo fatal e irrecuperable de la aplicación).

El patrón que he creado (y para el que he escrito un fragmento de código) es el siguiente:

#region IDisposable implementation

//TODO remember to make this class inherit from IDisposable -> $className$ : IDisposable

// Default initialization for a bool is 'false'
private bool IsDisposed { get; set; }

/// <summary>
/// Implementation of Dispose according to .NET Framework Design Guidelines.
/// </summary>
/// <remarks>Do not make this method virtual.
/// A derived class should not be able to override this method.
/// </remarks>
public void Dispose()
{
    Dispose( true );

    // This object will be cleaned up by the Dispose method.
    // Therefore, you should call GC.SupressFinalize to
    // take this object off the finalization queue 
    // and prevent finalization code for this object
    // from executing a second time.

    // Always use SuppressFinalize() in case a subclass
    // of this type implements a finalizer.
    GC.SuppressFinalize( this );
}

/// <summary>
/// Overloaded Implementation of Dispose.
/// </summary>
/// <param name="isDisposing"></param>
/// <remarks>
/// <para><list type="bulleted">Dispose(bool isDisposing) executes in two distinct scenarios.
/// <item>If <paramref name="isDisposing"/> equals true, the method has been called directly
/// or indirectly by a user's code. Managed and unmanaged resources
/// can be disposed.</item>
/// <item>If <paramref name="isDisposing"/> equals false, the method has been called by the 
/// runtime from inside the finalizer and you should not reference 
/// other objects. Only unmanaged resources can be disposed.</item></list></para>
/// </remarks>
protected virtual void Dispose( bool isDisposing )
{
    // TODO If you need thread safety, use a lock around these 
    // operations, as well as in your methods that use the resource.
    try
    {
        if( !this.IsDisposed )
        {
            if( isDisposing )
            {
                // TODO Release all managed resources here

                $end$
            }

            // TODO Release all unmanaged resources here



            // TODO explicitly set root references to null to expressly tell the GarbageCollector
            // that the resources have been disposed of and its ok to release the memory allocated for them.


        }
    }
    finally
    {
        // explicitly call the base class Dispose implementation
        base.Dispose( isDisposing );

        this.IsDisposed = true;
    }
}

//TODO Uncomment this code if this class will contain members which are UNmanaged
// 
///// <summary>Finalizer for $className$</summary>
///// <remarks>This finalizer will run only if the Dispose method does not get called.
///// It gives your base class the opportunity to finalize.
///// DO NOT provide finalizers in types derived from this class.
///// All code executed within a Finalizer MUST be thread-safe!</remarks>
//  ~$className$()
//  {
//     Dispose( false );
//  }
#endregion IDisposable implementation

Aquí está el código para implementar IDisposable en una clase derivada. Tenga en cuenta que no es necesario incluir explícitamente la herencia de IDisposable en la definición de la clase derivada.

public DerivedClass : BaseClass, IDisposable (remove the IDisposable because it is inherited from BaseClass)


protected override void Dispose( bool isDisposing )
{
    try
    {
        if ( !this.IsDisposed )
        {
            if ( isDisposing )
            {
                // Release all managed resources here

            }
        }
    }
    finally
    {
        // explicitly call the base class Dispose implementation
        base.Dispose( isDisposing );
    }
}

Publiqué esta implementación en mi blog en: Cómo implementar correctamente el patrón de eliminación

Dave Black avatar Jan 13 '2010 21:01 Dave Black

Estoy de acuerdo con pm100 (y debería haberlo dicho explícitamente en mi publicación anterior).

Nunca debes implementar IDisposable en una clase a menos que lo necesites. Para ser muy específico, hay alrededor de 5 ocasiones en las que alguna vez necesitarías/deberías implementar IDisposable:

  1. Su clase contiene explícitamente (es decir, no a través de herencia) cualquier recurso administrado que implemente IDisposable y debe limpiarse una vez que su clase ya no se use. Por ejemplo, si su clase contiene una instancia de Stream, DbCommand, DataTable, etc.

  2. Su clase contiene explícitamente cualquier recurso administrado que implemente un método Close(), por ejemplo, IDataReader, IDbConnection, etc. Tenga en cuenta que algunas de estas clases implementan IDisposable al tener Dispose() así como un método Close().

  3. Su clase contiene explícitamente un recurso no administrado, por ejemplo, un objeto COM, punteros (sí, puede usar punteros en C# administrado pero deben declararse en bloques "no seguros", etc. En el caso de recursos no administrados, también debe asegurarse de llame a System.Runtime.InteropServices.Marshal.ReleaseComObject() en el RCW. Aunque el RCW es, en teoría, un contenedor administrado, todavía hay un recuento de referencias bajo las sábanas.

  4. Si su clase se suscribe a eventos utilizando referencias sólidas. Necesitas darte de baja/desvincularte de los eventos. ¡Siempre asegúrese de que estos no sean nulos antes de intentar cancelar el registro o separarlos!

  5. Su clase contiene cualquier combinación de lo anterior...

Una alternativa recomendada para trabajar con objetos COM y tener que usar Marshal.ReleaseComObject() es usar la clase System.Runtime.InteropServices.SafeHandle.

El BCL (equipo de biblioteca de clase base) tiene una buena publicación de blog al respecto aquí http://blogs.msdn.com/bclteam/archive/2005/03/16/396900.aspx

Una nota muy importante a tener en cuenta es que si está trabajando con WCF y limpiando recursos, CASI SIEMPRE debe evitar el bloque "usar". Hay muchas publicaciones en blogs y algunas en MSDN sobre por qué esto es una mala idea. También publiqué sobre esto aquí: no use 'using()' con un proxy WCF

Dave Black avatar Jan 14 '2010 00:01 Dave Black