CodeGym /Cursos /C# SELF /Flujos de entrada/salida: ...

Flujos de entrada/salida: Stream

C# SELF
Nivel 36 , Lección 1
Disponible

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
Esquema de herencia de flujos en .NET
  • 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
CanRead
¿Se puede leer de este flujo?
CanWrite
¿Se puede escribir en este flujo?
CanSeek
¿Se puede mover la posición en el flujo? (no todos lo soportan)
Length
Longitud del flujo (si se soporta — no todos los flujos la tienen)
Position
Posición actual en el flujo
Read(...)
Lectura de datos
Write(...)
Escritura de datos
Seek(...)
Moverse por el flujo
Flush()
Vaciar el buffer (escribir todo lo acumulado en el flujo)
Close()
/
Dispose()
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
}
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION