Implementar IDisposable correctamente

En este post vamos a ver en detalle qué es la interfaz IDisposable, también aprenderemos a utilizarla con un ejemplo dentro de C#. 

 

 

1 - Qué es la interfaz IDIsposable

La interfaz IDisposable es una interfaz muy simple, si no sabes lo que son las interfaces

La interfaz IDisposable, también es muy sencilla, es una interfaz con un único método el cual nos provee de un mecanismo para liberar objetos de la memoria que no estén administrados por el sistema. 

public interface IDisposable
{
    void Dispose();
}

El punto importante sobre esta interfaz es que está definida en el core del lenguaje .NET, lo que implica que cualquier clase dentro de nuestros programas pueden implementar IDisposable.

 

Y necesitas saber si estás utilizando una instancia de una clase que es disposable (desechable) para poder utilizar el patrón correctamente.

 

1.1 - Recursos no administrados por el sistema

Cuando implementamos IDisposable, le estamos diciendo a los consumidores de nuestra clase que estamos utilizando recursos no administrados por el sistema, y debido a eso estos recursos deben ser indicados de forma manual para ser limpiados por el garbage collector (recolector de basura). 

 

Implementar, utilizar y comprender IDisposable es muy sencillo, pero la documentación es muy confusa ya que está siempre haciendo referencia a “recursos no administrados por el sistema” lo que te hace pensar que no te afecta, pero en verdad si. 

 

Cuando trabajamos en una aplicación en .NET y esta aplicación, tiene contacto con el sistema de ficheros o una consulta SQL o la mayoría de los servicios de la nube, como puede ser S3 en amazon web services está utilizando recursos no administrados por el sistema. 

recursos no adminsitrados

Ejemplo: en el caso concreto de la conexión SQL utilizamos System.Data que si está administrado por el sistema, pero la conexión a la base de datos como tal NO lo está, por lo que debemos invocar Dispose para liberar la memoria. 

 

Pero no solo sirve con llamar al método Dispose(), sino que debemos saber que hacer en él, por ejemplo en una conexión a la base de datos, debemos cerrar dicha conexión. 

 

 

2 - Cómo utilizar IDisposable

Si implementamos una instancia de una clase la cual implementa IDisposable, debemos administrar manualmente su tiempo de vida. 

Para ello la instanciamos dentro de un bloque using, el cual, por detrás compila como un try{}finally{}  llamando al método .Dispose() dentro del finally de forma automática. 

public class EjemploClaseDisposable : IDisposable
{
    public void Metodo1Ejemplo()
    {
        Console.WriteLine("Ejemplo");
    }

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

[TestMethod]
public void EjemploUsing()
{
    using (EjemploClaseDisposable ej = new EjemploClaseDisposable())
    {
        ej.Metodo1Ejemplo();
    }
}

Llamar al método dispose significa indicar al garbage collector que debe pasar a liberar los recursos utilizados. 

 

El ejemplo que acabamos de ver es equivalente a crear el objeto, utilizarlo y luego llamar a dispose: 

[TestMethod]
public void EjemploInstanciar()
{
    EjemploClaseDisposable ej = new EjemploClaseDisposable();
    ej.Metodo1Ejemplo();
    ej.Dispose();
}

Es recomendable llamar al método .Dispose() lo más pronto que podamos en nuestro código, para así liberar el recurso.

 

En la mayoría de casos de uso reales, nuestro problema será con la memoria, pero por ejemplo, podemos tenerlo también conectando a la base de datos. Cuando realizamos llamadas a la base de datos estas están limitadas a un número, el cual llamamos “pool de conexiones” una vez pasamos este número, la aplicación se cuelga, veremos un ejemplo mas adelante.

 

2.1 - Qué pasa si no utilizamos Dispose

En el caso de que no utilicemos el .Dispose() o no instanciemos el objeto dentro de un bloque using no recibiremos ningún error. Ni en tiempo de ejecución ni en tiempo de compilación.

 

Pero tendremos un problema en nuestro código, probablemente no seamos capaz de detectarlo rápidamente, ya que dependerá mucho sobre qué hace la clase y cuanta memoria consume. Ya que no estamos indicando al garbage collector que limpie los recursos cuando acaba de utilizarlos. 

 

En los peores escenarios, una clase puede estar apuntando o contener una referencia a una objeto que debería haber sido limpiado, pero este no ha sido Disposed por lo que esa memoria nunca será liberada hasta que la ejecución de la aplicación termine, o salte alguna excepción como OutOfMemoryException o cualquier otra. 

 

Este tipo de errores es difícil de detectarlos, o al menos no se detectan de forma muy rápida ya que es posible que en nuestra máquina y en las de test funcione, pero cuando ponemos el código en producción peta, y esto es debido a que en producción tiene una mayor carga de trabajo. 

 

 

3 - Ejemplo IDisposable

Para este ejemplo he intentado hacer un ejemplo “común” dentro del mundo empresarial.

El supuesto es que tenemos que consultar una base de datos. En este ejemplo simplemente leo la fecha actual (para no perder tiempo en crear una tabla y datos en ella).

Vamos a utilizar la conexión que vimos en el post de como conectarse a una base de datos.

 

Para ello disponemos de una clase que nos va a permitir hacer consultas a la base de datos, para simplificar únicamente voy a recibir la fecha actual. Y si hacemos una única llamada, vemos como funciona correctamente

public class DatabaseWrapper
{
    private MySqlConnection _connection;

    public string GetFecha()
    {
        if (_connection == null)
        {
            _connection = new MySqlConnection("Server=127.0.0.1;Port=3306;Database=personal;Uid=root;password=test;");
            _connection.Open();
        }
        using (var command = _connection.CreateCommand())
        {
            command.CommandText = "SELECT NOW()";
            return command.ExecuteScalar().ToString();
        }
    }
}

[TestMethod]
public void EjemploejecutarDb()
{
    var wrapper = new DatabaseWrapper();
    var fecha = wrapper.GetFecha();
    Console.WriteLine(fecha);
    Assert.IsTrue(true);
}

El problema viene cuando en vez de una sola llamada, realizamos un bucle con mil llamadas.

[TestMethod]
public void EjemploLeerNoDisposed()
{
    for (int i = 0; i < 1000; i++)
    {
        var wrapper = new DatabaseWrapper();
        var fecha = wrapper.GetFecha();
        Console.WriteLine(fecha);
    }
    Assert.IsTrue(true);
}

Vemos cómo este código no funciona y el error que nos da es un timeout en la base de datos indicando que hemos alcanzado el número máximo de conexiones ya que estas NUNCA se liberan. 

Test method ProgramacionAvanzada.Test.Test_IDisposable.EjemploEjecutarDbNoDisposable threw exception: 
MySql.Data.MySqlClient.MySqlException: error connecting: Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached.

Además podemos consultar la base de datos y nos dará ese número máximo que tenemos.

USE information_schema;
SELECT COUNT(*) FROM PROCESSLIST;

Para .NET podemos cambiar el número máximo de conexiones a la base de datos cuando indicamos la conexión añadiendo el atributo “Max pool size={número}” pero obviamente esto no es una solución aceptable, ya que eventualmente puede ir a más y a más. 

 

Lo que debemos hacer para arreglar este entuerto es implementar la interfaz IDisposable,

cuando implementamos dicha interfaz le estamos indicando al código que tiene que liberar dicha conexión cuando termina la ejecución. 

public class DatabaseWrapperDispose : IDisposable
{
    private MySqlConnection _connection;

    public string GetFecha()
    {
        if (_connection == null)
        {
            _connection = new MySqlConnection("Server=127.0.0.1;Port=3306;Database=personal;Uid=root;password=test;");
            _connection.Open();
        }
        using (var command = _connection.CreateCommand())
        {
            command.CommandText = "SELECT NOW()";
            return command.ExecuteScalar().ToString();
        }
    }

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

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            _connection.Close();
            _connection.Dispose();
        }
    }

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

Y por supuesto debemos recordar el ejecutar el código utilizando nuestro bloque using o un try{} finally{}

[TestMethod]
public void EjemploLeerDisposed()
{
    for (int i = 0; i < 1000; i++)
    {
        using (var wrapper = new DatabaseWrapperDispose())
        {
            var fecha = wrapper.GetFecha();
            Console.WriteLine(fecha);
        }
    }
    Assert.IsTrue(true);
}

[TestMethod]
public void EjemploLeerDisposedTryFinally()
{
    for (int i = 0; i < 1000; i++)
    {
        DatabaseWrapperDispose wrapper = null;
        try
        {
            wrapper = new DatabaseWrapperDispose();
            var fecha = wrapper.GetFecha();
        }
        finally
        {
            wrapper.Dispose();
        }
    }
    Assert.IsTrue(true);
}

 

 

Conclusión

Muchos programas llegan a tener serios problemas porque no utilizan Dispose de forma correcta o porque no se implementa de ninguna de las maneras. 

 

El uso correcto de Dispose es complicado, ya que debemos saber y entender cada una de las clases que utilizamos, pero no únicamente las nuestras, sino todas las que utiliza el propio framework. 

 

Utilizar bien Dispose nos da una estabilidad en el código, que hoy en dia debido a que por ejemplo AWS cuando utilizas lambda (o azure con azure functions) te cobra por memoria usada así como tiempo de ejecución, un problema en el uso de la memoria puede llevar a consumir miles de euros adicionales. 

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café