Un Stream es una abstracción que representa una secuencia de bytes que podemos leer o escribir sin importar de dónde vienen o adónde van.
Puedes pensar en un stream como una tubería que conecta tu código con un origen o destino de datos. Por un extremo entran bytes y por el otro salen, sin necesidad de cargar todo el contenido en memoria.
Esto es especialmente importante si tienes que procesar, por ejemplo, un vídeo de 10 GB. No puedes cargarlo entero en la RAM (byte[]), pero sí leerlo poco a poco mientras fluye.
La clase base System.IO.Stream
Stream es una clase abstracta, por lo que no puedes hacer new Stream(). Usamos implementaciones concretas como FileStream o MemoryStream, aunque podemos tratarlas mediante el tipo base cuando solo necesitamos las operaciones comunes.
Sus operaciones principales son:
Read: lee bytes del flujo y los copia a un búfer.Write: escribe en el flujo los bytes de un búfer.Seek: mueve la posición actual si el flujo admite acceso aleatorio.Flush: envía al destino los datos pendientes en el búfer.
Un stream maneja bytes (byte). No sabe qué es una letra A ni un número entero 100. Para trabajar con texto o tipos de datos usamos clases como StreamReader, BinaryReader o StreamWriter sobre el flujo.
Tipos de streams habituales
Dependiendo de dónde estén los datos, usamos una implementación u otra.
FileStream, datos en disco
FileStream conecta el código con un archivo del sistema. Es una de las clases que utiliza System.IO.File internamente.
// Abrimos un stream para leer un archivo
using (FileStream fs = new FileStream("video.mp4", FileMode.Open))
{
Console.WriteLine($"Tamaño del archivo: {fs.Length} bytes");
}MemoryStream, datos en memoria
MemoryStream crea un flujo que vive dentro de la memoria RAM. Resulta útil cuando una función pide un Stream, pero ya tienes los datos en un array de bytes y no quieres pasar por el disco.
byte[] datos = { 1, 2, 3, 4, 5 };
using (MemoryStream ms = new MemoryStream(datos))
{
int primerByte = ms.ReadByte();
}NetworkStream, datos de red
NetworkStream se usa en comunicaciones TCP. En este caso el concepto de flujo es literal: los datos llegan por la red poco a poco.
Ten en cuenta que un flujo de red normalmente no permite usar Seek, porque no podemos retroceder para volver a recibir unos bytes que ya han pasado.
El búfer
Leer byte a byte es muy ineficiente. Cada petición puede implicar trabajo adicional para el sistema operativo o el dispositivo de entrada.
Por eso leemos bloques de datos mediante un búfer, un espacio temporal en memoria que reduce el número de operaciones:
using (FileStream fs = new FileStream("datos.bin", FileMode.Open))
{
byte[] buffer = new byte[1024];
int bytesLeidos;
while ((bytesLeidos = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// Procesamos únicamente la parte válida del búfer
ProcesarDatos(buffer, bytesLeidos);
}
}Si solo necesitas copiar un flujo completo en otro, CopyTo ya implementa este bucle y gestiona el búfer por ti.
using (var origen = new FileStream("in.dat", FileMode.Open))
using (var destino = new FileStream("out.dat", FileMode.Create))
{
origen.CopyTo(destino);
}Encadenar streams
Los streams se pueden envolver para añadir transformaciones. Este diseño permite combinar responsabilidades sin que cada clase conozca el destino final.
Por ejemplo, podemos escribir texto comprimido en un archivo mediante tres capas:
FileStreamescribe en el archivo.GZipStreamcomprime los bytes.StreamWriterconvierte el texto en bytes.
using System.IO.Compression;
using (FileStream fs = new FileStream("archivo.gz", FileMode.Create))
using (GZipStream compresor = new GZipStream(fs, CompressionLevel.Optimal))
using (StreamWriter escritor = new StreamWriter(compresor))
{
escritor.WriteLine("Hola mundo comprimido");
}Al StreamWriter no le importa si escribe en un archivo, en memoria o en una conexión comprimida. Solo necesita recibir otro stream escribible.
Seek y Position
Un stream tiene una posición interna accesible mediante Position. Cuando lees o escribes bytes, esa posición avanza.
Si el stream admite búsquedas (puedes comprobarlo con CanSeek), puedes volver atrás o saltar a una posición concreta:
using (MemoryStream ms = new MemoryStream())
{
ms.WriteByte(65); // Escribe 'A' y avanza a la posición 1
ms.Position = 0; // Volvemos al principio
int leido = ms.ReadByte(); // Lee 65 ('A')
}Dispose y Flush
Los streams pueden mantener abiertos archivos, sockets y otros recursos del sistema. Por eso debes liberarlos siempre al terminar.
La forma habitual es usar una declaración o un bloque using, que llama a Dispose() aunque se produzca una excepción. Al cerrar un stream también se vuelcan sus búferes pendientes.
Flush() permite solicitar ese volcado sin cerrar el flujo. Aun así, no debes asumir que garantiza por sí solo que los datos hayan llegado al almacenamiento físico, ya que pueden existir otros niveles de caché.