1. Introducción
Vamos a imaginar una tetera con agua. Abres el grifo — el agua empieza a fluir. Puedes llenar mucha agua y verterla toda de golpe, o puedes llenar la tetera poco a poco. Lo mismo pasa con los archivos — no siempre es cómodo o posible cargar todo el archivo en la memoria de una vez. Los archivos pueden ser grandes, y a veces la fuente de datos ni siquiera es un archivo, sino, por ejemplo, una conexión de red donde los datos llegan poco a poco.
Si siempre intentáramos trabajar solo con arrays de bytes, con archivos grandes nos quedaríamos sin memoria muy rápido, y para flujos de datos "infinitos" (como streams de vídeo o audio) este enfoque simplemente no funciona. ¡Aquí es donde entra en juego el concepto de flujo!
En .NET, un flujo es una abstracción para acceder a los datos de forma secuencial: no importa qué hay detrás de la fuente — un archivo, la red, la memoria, o incluso algo exótico como un archivo comprimido. El flujo te permite leer y escribir datos por partes, normalmente en bloques o bytes.
Idea principal:
- Flujo — es un canal para transferir datos. Es como una cinta transportadora: puedes "poner" (escribir) o "sacar" (leer) datos, sin preocuparte directamente de los detalles de dónde y cómo se almacenan.
- Los datos llegan de forma secuencial: solo puedes leer el siguiente trozo después del anterior (o al revés, si se permite rebobinar).
- En la mayoría de los casos no guardas todos los datos en memoria a la vez (y tu ordenador te lo agradecerá).
Esta abstracción está en la base de casi todas las operaciones de entrada/salida en .NET: trabajar con archivos, redes, archivos comprimidos, ¡incluso con la consola!
2. Flujos System.IO.Stream
Herencia y arquitectura: System.IO.Stream
Casi todos los flujos en .NET heredan de la clase abstracta System.IO.Stream. Define los métodos principales para leer, escribir, moverse por el flujo y gestionarlo.
classDiagram
class Stream {
+Read()
+Write()
+Seek()
+CanRead
+CanWrite
+CanSeek
+Length
+Position
}
class FileStream
class MemoryStream
class NetworkStream
class CryptoStream
Stream <|-- FileStream
Stream <|-- MemoryStream
Stream <|-- NetworkStream
Stream <|-- CryptoStream
- Stream — clase base abstracta
- FileStream — para trabajar con archivos
- MemoryStream — para trabajar con datos en memoria
- NetworkStream — para interacción de red
- CryptoStream — para cifrado/descifrado
Breve vistazo a las propiedades y métodos clave del flujo
| Propiedad / Método | Descripción |
|---|---|
|
¿Se puede leer de este flujo? |
|
¿Se puede escribir en este flujo? |
|
¿Se puede mover la posición en el flujo? (no todos lo soportan) |
|
Longitud del flujo (si se soporta — no todos los flujos la tienen) |
|
Posición actual en el flujo |
|
Lectura de datos |
|
Escritura de datos |
|
Moverse por el flujo |
|
Vaciar el buffer (escribir todo lo acumulado en el flujo) |
/ |
Cerrar el flujo y liberar recursos |
Vamos a ver cómo se ve esto "en la práctica".
3. Ejemplo: leer y escribir archivos usando Stream
Aquí tienes un ejemplo bastante minimalista para ver un flujo "en acción":
// Abrimos un archivo para escribir
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Imagina que queremos escribir los números del 1 al 10 en el archivo
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Escribimos byte a byte
}
// Cerramos explícitamente el archivo para poder abrirlo para leer
stream.Close();
// Ahora intentamos leer estos números de vuelta
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Mostrará 1, 2, ... 10
}
Aquí usamos FileStream, que es un flujo real en todo el sentido de la palabra: lees y escribes datos en bloques o byte a byte.
Tipos de flujos: ¿dónde pueden aparecer?
Un flujo no tiene por qué ser solo un archivo en disco. Aquí tienes algunos ejemplos donde se usa el concepto de flujo:
- Archivo en disco (por ejemplo, FileStream — el caso más común)
- Flujo en memoria RAM (MemoryStream — útil para datos temporales o intermedios)
- Conexión de red (NetworkStream)
- Compresión/archivado (GZipStream, DeflateStream)
- Cifrado (CryptoStream)
- Entrada/salida de consola (¡sí, también!) — técnicamente, también son flujos
Esto te permite escribir código sin preocuparte por la fuente/destino concreto de los datos: si tu código trabaja con un flujo, ¡es universal!
4. Detalles útiles
Leer y escribir son operaciones de transferencia de datos por partes. Normalmente a través de arrays de bytes y los métodos Read, Write.
Ejemplo: leer un archivo por bloques
byte[] buffer = new byte[1024]; // Buffer de 1024 bytes (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Procesamos solo bytesRead bytes dentro de buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Suma del bloque: {sum}");
}
Este enfoque se usa en todas partes — desde antivirus hasta reproductores de música.
Posicionamiento en el flujo (Position, Seek)
En la mayoría de las implementaciones de flujos (por ejemplo, los de archivos) puedes moverte por los datos — leer no solo el "siguiente trozo", sino saltar a una posición concreta y trabajar desde ahí.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Nos movemos al sexto byte (la numeración empieza en 0)
int value = stream.ReadByte();
Console.WriteLine($"6º byte en el archivo: {value}");
Los flujos pueden ser solo de lectura, solo de escritura, o ambos
Algunos flujos solo soportan una de las opciones:
- Archivo abierto para escribir: solo Write()
- Flujo para leer datos de red: solo Read()
- En algunos casos exóticos (por ejemplo, flujo para imprimir en impresora) no es posible "rebobinar" o posicionarse en el flujo (no se puede ir hacia atrás).
Comprueba las operaciones soportadas usando las propiedades CanRead, CanWrite, CanSeek:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Lectura soportada");
if (stream.CanWrite)
Console.WriteLine("Escritura soportada");
if (stream.CanSeek)
Console.WriteLine("Se puede mover por el archivo");
Bufferización en los flujos
Casi todos los flujos usan buffers internos para mejorar el rendimiento. La bufferización ahorra accesos al disco/red: los datos se acumulan internamente y luego se leen/escriben de golpe.
El método Flush() permite vaciar el buffer (por ejemplo, para garantizar que todo se ha escrito en disco):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("¡Hola, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Garantiza que la escritura se ha realizado en disco
Si escribes datos críticos (por ejemplo, transacciones de pago), llamar a Flush() es tu amigo.
5. Errores típicos al trabajar con flujos
Muy a menudo los principiantes cometen los siguientes errores:
Se olvidan de cerrar el flujo (y acaban con fugas de memoria, archivos "colgados" y otras sorpresas).
Confunden flujos de texto y binarios — intentan escribir una cadena con un método de bytes y luego obtienen "caracteres raros".
Usan un buffer demasiado pequeño (o sin buffer) — las operaciones se vuelven lentas.
Piensan que Read() siempre lee exactamente el número de bytes solicitado — en realidad, puede devolver menos; siempre hay que comprobar el valor devuelto.
No tienen en cuenta que no todos los flujos soportan posicionamiento (Seek), especialmente los de red.
Por ejemplo:
// Mal ejemplo: leer todos los bytes de un archivo sin comprobar cuántos bytes se leyeron realmente
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead puede ser menor que 1024 si el archivo es más pequeño
}
GO TO FULL VERSION