Menu

Sentencia using en C#: IDisposable, Dispose y using var

La sentencia using garantiza que se llama a Dispose en archivos, streams, conexiones y demás objetos IDisposable, incluso cuando se lanza una excepción. Aprende en qué se compila, en qué orden se liberan los objetos, la declaración using de C# 8 y cómo implementar IDisposable en tu propia clase.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Algunos objetos contienen recursos que el recolector de basura no gestiona: archivos abiertos, sockets de red, conexiones a bases de datos, descriptores del sistema operativo. Implementan IDisposable, y liberas el recurso llamando a Dispose(). La sentencia using lo llama por ti en un punto fijo, aunque se lance una excepción.

Esta página trata la sentencia using. Las líneas using System; del principio de un archivo son otra característica, la directiva using, que importa un namespace.

Un bloque using

using (declaration) { body } crea el objeto, ejecuta el cuerpo y después llama a Dispose. Una clase pequeña que imprime desde Dispose muestra exactamente cuándo ocurre:

Salida:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

El segundo bloque lanza una excepción, y "close api" se imprime igualmente, antes de que el catch maneje la excepción. Esa garantía es la razón para usar using en lugar de llamar tú mismo a Dispose() en la última línea: cualquier excepción lanzada antes se salta una llamada manual.

En qué se compila using

El compilador expande un bloque using en un try/finally. Estos dos son equivalentes:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

De la expansión salen tres detalles. La variable tiene como ámbito el bloque y es de solo lectura dentro de él (no puedes reasignar writer). El objeto debe implementar IDisposable, o el código no compila. Y se permite un valor null: simplemente se salta Dispose, lo que resulta útil cuando un método de fábrica podría no devolver nada.

Archivos y streams

Las clases de archivos y streams son el caso del día a día. Un StreamWriter guarda texto en un buffer en memoria, y Dispose vuelca el buffer al disco y cierra el descriptor del archivo:

Salida:

id,total
1001,59.90
1002,12.50

Sin Dispose, las últimas líneas del writer pueden seguir en su buffer, y en Windows el descriptor abierto impide que otros programas (y tu propio File.Delete siguiente) toquen el archivo. Métodos auxiliares como File.WriteAllText y File.ReadAllLines abren y liberan internamente, así que no necesitan using. Consulta archivos para más información.

Varios recursos: orden de liberación

Apila sentencias using sin llaves entre ellas para abrir varios recursos en un bloque. Se liberan en orden inverso al de creación:

Salida:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

El orden inverso es lo que necesitan los recursos en capas: un writer envuelve un buffer que envuelve un archivo, así que el writer debe volcar en el buffer antes de que el buffer escriba en el archivo, y esto antes de que se cierre el archivo.

La declaración using (C# 8)

C# 8 añadió una forma sin llaves. La variable se libera cuando termina el ámbito que la contiene, normalmente el final del método:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Elimina un nivel de indentación, lo que se nota cuando un método usa dos o tres recursos. La regla del orden de liberación es la misma: las declaraciones de un mismo ámbito se liberan en orden inverso. La contrapartida es que el recurso sigue abierto hasta que termina el ámbito, así que cuando un archivo debe cerrarse antes de que el método haga más trabajo, mantén la forma de bloque o pon la declaración en su propio bloque { }.

Para los objetos cuya limpieza es asíncrona (IAsyncDisposable, como muchas conexiones a bases de datos y muchos streams), C# 8 también tiene await using, que espera a DisposeAsync() en el mismo lugar.

Implementar IDisposable en tu propia clase

Implementa IDisposable cuando tu clase es dueña de algo liberable (creó un stream, un temporizador, una conexión) y por tanto debe liberarlo a su vez. En el caso habitual, es un método corto:

Salida:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

Las reglas que sigue: Dispose libera todo lo que posee el objeto; llamarlo más de una vez no hace nada la segunda vez; y usar el objeto después de liberarlo lanza ObjectDisposedException. using (log) también muestra que el bloque puede recibir una variable existente en lugar de una declaración.

El "patrón dispose" más largo, con un método protected virtual void Dispose(bool disposing) y un finalizador, solo hace falta cuando una clase contiene directamente un descriptor no administrado (un puntero en bruto de código nativo). En el .NET moderno, envuelve esos descriptores en una subclase de SafeHandle, y la versión sencilla de arriba es todo lo que tu clase necesita.

Errores comunes

  • Devolver un objeto creado en un bloque using. Se libera al salir del bloque, así que quien llama recibe un stream cerrado. Devuelve los datos, o deja que quien llama sea el dueño del using.
  • Olvidar using en streams y writers. Se pueden perder datos en buffer y los archivos se quedan bloqueados.
  • Liberar un objeto compartido. Libera solo lo que es tuyo. Un HttpClient de larga vida, por ejemplo, está pensado para compartirse y reutilizarse, no para crearse y liberarse en cada petición.
  • Usar un objeto después del bloque. Los métodos de un objeto liberado lanzan ObjectDisposedException.
  • Confiar en el recolector de basura. Libera memoria, no archivos ni sockets, y nunca llama a Dispose.

Preguntas frecuentes

¿Qué hace la sentencia using en C#?

using (var x = ...) { ... } llama a x.Dispose() cuando termina el bloque, tanto si termina con normalidad como con un return o porque se lanzó una excepción. El compilador la convierte en un try/finally con la llamada a Dispose en el finally, así que el recurso se libera en un punto conocido y no cuando le toque ejecutarse al recolector de basura.

¿Qué es IDisposable en C#?

IDisposable es una interfaz con un solo método, void Dispose(). Una clase la implementa cuando contiene algo que debe liberarse de forma explícita: un descriptor de archivo, un socket de red, una conexión a base de datos, un temporizador. Cualquier objeto cuyo tipo implemente IDisposable debería liberarse cuando terminas con él, normalmente con using.

¿Qué diferencia hay entre using y using var?

using (var x = ...) { } libera al final de su bloque. La declaración using using var x = ...; (C# 8 y posteriores) no tiene bloque propio: libera cuando termina el ámbito que la contiene, normalmente el final del método. Ahorra anidamiento cuando un recurso debe vivir el resto del método.

¿En qué orden se liberan varios objetos using?

En orden inverso al de creación. Con using (var a = ...) using (var b = ...) { }, se libera primero b y después a. Es el orden que quieres cuando b depende de a, como un StreamWriter que envuelve un FileStream.

¿Llama el recolector de basura a Dispose?

No. El recolector de basura libera memoria, y puede ejecutar un finalizador en algún momento posterior impredecible, pero nunca llama a Dispose. Un archivo sin liberar puede quedarse bloqueado, y sus datos en buffer pueden no escribirse nunca. Por eso existe using.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR