1. ¿Cómo liberar recursos correctamente en C#?
Imagina el sistema operativo como un bibliotecario muy estricto. Coges un libro (abres un archivo/flujo), lo lees y luego... ¡te olvidas de devolverlo! El bibliotecario se enfada: "¿Cómo que el libro sigue contigo?!". Pues lo mismo pasa con los flujos. Un flujo abierto ocupa recursos: un descriptor de archivo, un trozo de memoria, y encima bloquea el archivo para otras apps.
Si no cierras el flujo, el resultado puede ir desde "algo no funciona" hasta "todo se ha roto, nadie puede escribir en ese archivo". Y si tu programa tiene muchos flujos sin cerrar, el sistema puede empezar a "perder" recursos y simplemente dejar de funcionar.
¿Dónde está el peligro?
- El archivo no se cierra, los comandos no llegan al disco (por ejemplo, al escribir — los datos pueden quedarse en el buffer).
- El archivo se bloquea para otros procesos — tus compis y otros programas se cabrean.
- Límite de descriptores: en Windows/Linux los procesos tienen límites de archivos/streams abiertos.
Interfaz IDisposable
Cualquier clase que trabaje con recursos no gestionados (flujos, archivos, bases de datos, sockets), debe implementar la interfaz IDisposable.
public interface IDisposable
{
void Dispose();
}
Dentro del método Dispose() normalmente se liberan todos esos recursos sufridos: el archivo por fin se cierra, las conexiones se cortan, la memoria se libera.
Los flujos son objetos que tienen en sus manos recursos importantes: archivos, conexiones, memoria. Si no los cierras, el archivo puede quedarse bloqueado (tu Word dirá "¡archivo abierto por otra aplicación!"), y el sistema — sin memoria. Por eso es superimportante liberar el flujo cuando terminas de usarlo.
En .NET los flujos implementan la interfaz IDisposable. Eso significa: hay que cerrarlos llamando a Dispose() (o simplemente usando el bloque using).
2. Opciones para cerrar un flujo: de peligroso a seguro
Opción 1. Cierre "a mano": ¡no lo hagas así!
Este es el método antiguo. Y aunque lo he mostrado en ejemplos anteriores, hoy en día casi nadie lo usa en desarrollo moderno :P
var stream = new FileStream("file.txt", FileMode.Open);
// Trabajamos con el flujo
stream.Close(); // o stream.Dispose()
Problema: si entre abrir y cerrar ocurre un error/excepción, el archivo se queda abierto y bloqueado. Es como si salieras corriendo de la biblioteca con el libro porque has olido pizza...
Opción 2. Usando try...finally
FileStream stream = null;
try
{
stream = new FileStream("file.txt", FileMode.Open);
// Trabajamos con el flujo
}
finally
{
if (stream != null)
stream.Dispose();
}
Esta opción es fiable: finally se ejecuta sí o sí, incluso si hay error. Pero, seamos sinceros, da pereza escribirlo así.
Opción 3. Bonito y seguro: operador using
Sintaxis clásica (using ( ... ) { ... })
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// Trabajamos con el flujo
}
// ¡Aquí stream.Dispose() se llama automáticamente!
La idea clave — todo lo que está dentro del bloque using trabaja con el flujo, y cuando el bloque termina — el archivo se cierra incluso si algo sale mal (por ejemplo, si hay una excepción).
Opción 4. Sintaxis moderna
Sintaxis moderna (using var)
using var stream = new FileStream("file.txt", FileMode.Open);
// Trabajamos con el flujo
// ... Dispose se llama automáticamente cuando la variable sale del ámbito
¡Genial! Así no tienes que meter más llaves ni sangrías de la cuenta.
¿Cómo funciona esto "por dentro"?
El operador using el compilador lo convierte en ese mismo try...finally, pero por ti. Broma: "using escribe código limpio por ti — ¿quizá pronto empiece a tomar café y a perderse en Stack Overflow?".
Diferencia entre using clásico y moderno
| Bloque using clásico | using var (declaración) | |
|---|---|---|
| Vista | |
|
| Ámbito | Dentro de las llaves del bloque | Hasta el final del bloque actual (método, bucle, etc.) |
| Brevedad | Un poco más verboso | Conciso, menos sangrías |
| Desde | C# 1.0 | C# 8.0 y superior |
3. ¿Qué son las declaraciones using?
Hace 5 años el mundo vio una forma nueva y concisa de trabajar con objetos IDisposable.
Declaración using — es cuando en vez de un bloque declaras una variable con la palabra clave using, y se liberará automáticamente al final del bloque actual (por ejemplo, método o bucle), y no al final de las llaves de un bloque extra.
using var stream = new FileStream("file.txt", FileMode.Open);
// Trabajamos con el flujo
Console.WriteLine(stream.Length);
// ¡Aquí el archivo sigue abierto!
// ... fin del método
// stream.Dispose() se llama aquí automáticamente
Diferencias clave respecto al using clásico:
- No hacen falta llaves, no se crea un bloque de código anidado.
- La variable está disponible hasta el final de todo el bloque donde se declara (normalmente — método, a veces — bucle, clase, si se declara a nivel de clase).
- La liberación del recurso ocurre solo cuando termina el bloque de ejecución.
¿Por qué mola?
- Menos niveles de anidación — el código es mucho más corto y legible.
- Más fácil trabajar con varios recursos — declara varias variables using seguidas, y todo se libera cuando termina el método.
- Menos posibilidades de liarla — no te saltas el ámbito donde deberías llamar a Dispose().
4. Comparando: using clásico vs. moderno
Echemos un vistazo a la comparación en código.
Forma clásica
using (var reader = new StreamReader("input.txt"))
{
using (var writer = new StreamWriter("output.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
}
} // Aquí ambos archivos se cerrarán
Forma moderna (C# 8+)
using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
// Ambos archivos se cierran aquí, al salir del método
¿A que es más simple? Sobre todo si tienes aún más anidaciones — la forma moderna te hace la vida mucho más fácil.
5. ¿Cuándo y dónde se llama a Dispose()?
Aquí muchos estudiantes se equivocan: piensan que Dispose se llama justo después de la línea de uso — ¡pero no es así!
Mira este ejemplo:
void MyMethod()
{
using var fileStream = new FileStream("data.bin", FileMode.Open);
// ... mucho código, quizá bucles y llamadas anidadas
// ¡fileStream sigue abierto aquí!
// Aquí puedes acceder a fileStream
}
// Aquí, en el } del método, se llama a fileStream.Dispose()
Punto importante: Si declaras una variable using dentro de un bucle, Dispose se llama tras cada iteración.
foreach (var path in filePaths)
{
using var reader = new StreamReader(path);
// trabajamos con reader
} // reader.Dispose() se llama tras cada iteración (cierra el archivo)
6. Errores al migrar código antiguo
A veces pasa que migras código viejo o copias un ejemplo con using clásico, pero la variable necesita vivir más tiempo que el ámbito de las llaves. Entonces la opción clásica no te vale, pero las declaraciones using — perfectas.
Pero hay matices. Por ejemplo, si en un bucle tienes dos recursos, pero uno debe "vivir" más que el otro — decláralos en el orden correcto:
using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
using var resource2 = ...;
// resource2 vive una iteración
// resource1 — toda la función
}
7. Práctica
Vamos a seguir mejorando nuestra app de prácticas — un pequeño simulador de pedidos de café que llevas mejorando estos días. Ahora va a poder guardar el historial de pedidos en un archivo de texto y leerlos al arrancar.
Paso 1: Guardar el pedido en un archivo
using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("Café: Latte; Leche: Avena; Tamaño: Grande");
// El segundo parámetro del constructor de StreamWriter append: true indica que queremos añadir, no sobrescribir el archivo.
Paso 2: Leer el historial de pedidos
using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"Pedido: {line}");
}
Y en cuanto el programa termine este método, los archivos se cerrarán automáticamente.
8. Buenas prácticas con declaraciones using
1. Usa siempre using para objetos que implementan IDisposable
En .NET la mayoría de clases para trabajar con archivos, flujos, recursos implementan esta interfaz. Es la señal: ¡libérame usando using!
2. Ojo con el ámbito: no declares using-var donde la variable pueda "estorbar"
Si la variable solo se usa en un par de líneas — declárala ahí, y no antes.
3. No olvides el orden de liberación
Si declaras varias variables using seguidas — Dispose se llama en orden inverso:
using var first = new Resource("First");
using var second = new Resource("Second");
// ... trabajo
// Primero Dispose de second, luego de first
Esto a veces importa, si un recurso depende de otro (por ejemplo, el flujo de escritura debe liberarse antes que el archivo).
4. No uses declaraciones using fuera de un método
Las declaraciones using están prohibidas a nivel de clase (por ejemplo, para campos). Solo funcionan dentro de métodos, constructores, etc.
5. Combínalo con manejo de errores
Recuerda que incluso con using no todos los errores son cómodos — es buena idea añadir try-catch si necesitas controlar errores de lectura/escritura.
GO TO FULL VERSION