CodeGym /Cursos /C# SELF /Trabajo con flujos de bytes

Trabajo con flujos de bytes

C# SELF
Nivel 36 , Lección 4
Disponible

1. Introducción

Ya conocemos los flujos (Stream), que son una abstracción para leer o escribir datos de forma secuencial. Hemos trabajado con FileStream para acceder a archivos a nivel de bytes, y también hemos usado StreamReader y StreamWriter para trabajar cómodamente con datos de texto, que por dentro usan FileStream y se encargan de las codificaciones.

Pero, ¿qué pasa si necesitamos guardar en un archivo no solo texto, sino datos fuertemente tipados: números enteros (int), números de coma flotante (double, float), valores booleanos (bool), fechas (DateTime) o incluso estructuras personalizadas? Claro, podríamos convertir todo eso a cadenas y escribirlo usando StreamWriter, y luego hacer parsing al leer. Pero ese enfoque tiene desventajas importantes:

  • Ineficiencia de almacenamiento: El número 12345 escrito como texto ocupa 5 bytes (caracteres). En binario, un int ocupa solo 4 bytes. Para grandes volúmenes de datos, la diferencia es crítica.
  • Rendimiento: Las conversiones constantes de números a cadenas y viceversa son un gasto extra de tiempo de CPU.
  • Precisión de los datos: Convertir números decimales a texto y de vuelta puede causar pérdida de precisión por redondeo.
  • Complejidad del parsing: Desarmar manualmente cadenas de texto para extraer distintos tipos de datos (por ejemplo, "123,45 TRUE 2024-06-21") complica mucho el código y lo hace frágil.

Para resolver estos problemas existen las clases especializadas BinaryReader y BinaryWriter. Estas clases son adaptadores especializados que funcionan sobre cualquier Stream base (normalmente FileStream) y te dan métodos cómodos para leer y escribir tipos de datos primitivos de C# en su formato binario. Ellos se encargan de toda la conversión de bytes a tipos concretos y viceversa, simplificando mucho el trabajo con archivos binarios estructurados.

Idea clave: BinaryReader y BinaryWriter no son flujos independientes. Potencian la funcionalidad de un Stream existente, añadiendo métodos para trabajar con tipos de C# en vez de solo bytes crudos.

2. Escribir datos usando BinaryWriter

BinaryWriter ofrece un conjunto de métodos Write(), sobrecargados para cada tipo de dato primitivo de C#. Cuando llamas a uno de estos métodos, BinaryWriter convierte el valor a su representación binaria (una secuencia de bytes) y escribe esos bytes en el Stream base.

Ejemplo: Guardamos la configuración del juego

Imagina que queremos guardar la configuración del juego: nivel de volumen (número decimal), nivel actual del jugador (entero), si la música está activada (booleano) y el nivel de dificultad elegido (cadena).


string filePath = "settings.bin";

// 1. Creamos FileStream para escribir
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. Creamos BinaryWriter sobre FileStream, indicando la codificación para cadenas (si las hay)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. Escribimos distintos tipos de datos
writer.Write(0.75f);       // float (4 bytes)
writer.Write(15);          // int (4 bytes)
writer.Write(true);        // bool (1 byte)
writer.Write("Easy");      // string (prefijo de longitud + bytes)
                
Console.WriteLine($"Configuración guardada en '{filePath}'.");

Explicación del ejemplo:

  • Creamos un FileStream con FileMode.Create, que crea un archivo nuevo o sobrescribe el existente.
  • Luego creamos un BinaryWriter, pasándole fs. Importante: BinaryWriter por defecto cierra el flujo base (fs) cuando se llama a su método Dispose() (que se ejecuta automáticamente con el bloque using).
  • Los métodos writer.Write() son intuitivos: Write(float), Write(int), Write(bool), Write(string). Ellos saben cuántos bytes escribir para cada tipo y cómo representarlos.
  • Para las cadenas, BinaryWriter añade automáticamente un prefijo de longitud antes de los bytes de la cadena. Esto permite que BinaryReader sepa exactamente cuántos bytes leer para reconstruir la cadena.
  • Si intentas abrir settings.bin en un editor de texto verás "basura", porque es un archivo binario. Para ver el contenido necesitas un editor HEX.

3. Leer datos usando BinaryReader

BinaryReader ofrece métodos ReadXxx() (por ejemplo, ReadInt32(), ReadBoolean(), ReadString()), que leen la cantidad adecuada de bytes del Stream base y los convierten al tipo de dato de C# correspondiente.

Ejemplo: Cargamos la configuración del juego

Ahora vamos a leer la configuración desde el archivo settings.bin que creamos antes.


string filePath = "settings.bin";

// 1. Creamos FileStream para leer
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. Creamos BinaryReader sobre FileStream, usando la misma codificación
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

// 3. Leemos los datos en EL MISMO ORDEN en que fueron escritos
float volume = reader.ReadSingle();     // float
int level = reader.ReadInt32();         // int
bool isMusicOn = reader.ReadBoolean();  // bool
string difficulty = reader.ReadString(); // string
                
Console.WriteLine($"Configuración cargada de '{filePath}':");
Console.WriteLine($"- Volumen: {volume:P0}"); // Formateamos como porcentaje (usando string formatting)
Console.WriteLine($"- Nivel del jugador: {level}");
Console.WriteLine($"- Música activada: {isMusicOn}");
Console.WriteLine($"- Dificultad: {difficulty}");

Explicación del ejemplo:

  • Abrimos un FileStream en modo FileMode.Open para leer.
  • Creamos un BinaryReader sobre fs, usando la misma codificación que al escribir.
  • Muy importante: El orden de las llamadas a reader.ReadXxx() debe COINCIDIR EXACTAMENTE con el orden en que los datos fueron escritos con BinaryWriter. Si intentas leer un string donde se escribió un int, tendrás un error EndOfStreamException (si el string es más largo) o leerás datos incorrectos.
  • Los métodos ReadXxx() leen automáticamente la cantidad necesaria de bytes y los convierten al tipo solicitado. ReadString() usa ese prefijo de longitud que escribió BinaryWriter para saber cuántos bytes leer para la cadena completa.

4. Matices importantes y buenas prácticas

Orden estricto:

Esta es la regla principal. BinaryReader y BinaryWriter no guardan metadatos sobre los tipos; solo saben cuántos bytes ocupa cada tipo primitivo. Tú tienes que asegurar que el orden coincida.

Gestión de recursos (using):

Como la mayoría de las clases de .NET que trabajan con recursos del sistema (por ejemplo, archivos o conexiones de red), tanto BinaryReader como BinaryWriter implementan la interfaz IDisposable. Así que siempre ponlos en un bloque using — así te aseguras de que se llame automáticamente a Dispose(), incluso si ocurre un error. Esto te protege de fugas y cierra bien el archivo.

Por cierto, por defecto BinaryWriter y BinaryReader también llaman a Dispose() del flujo base que les pasas (por ejemplo, FileStream), así que también se cerrará automáticamente.


using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... trabajo

Codificación para cadenas:

Para que funcione bien con cadenas pasadas a BinaryWriter.Write(string) y leídas con BinaryReader.ReadString(), asegúrate de usar la misma codificación en sus constructores (por ejemplo, Encoding.UTF8). Si no, puedes tener problemas con caracteres fuera de ASCII.

Manejo de excepciones:

Las operaciones de entrada/salida de archivos siempre pueden fallar por factores externos (archivo no existe, sin permisos, disco lleno). Siempre pon el código con FileStream y BinaryReader/BinaryWriter en bloques try-catch para mayor fiabilidad.

BaseStream y posición:

Puedes acceder al flujo base a través de la propiedad BaseStream (por ejemplo, reader.BaseStream o writer.BaseStream). Esto es útil si quieres saber la posición actual (BaseStream.Position) o moverte por el archivo (BaseStream.Seek()).


// Ejemplo de uso de BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(123);
Console.WriteLine($"Posición actual en el flujo: {writer.BaseStream.Position}"); // Mostrará 4 (tamaño de int)

writer.Write("Hello");
Console.WriteLine($"Posición actual en el flujo: {writer.BaseStream.Position}"); // Mostrará 4 + (1+5) = 10

⚠️ El método Write(string) primero escribe la longitud de la cadena como un entero de 7 bits, y luego los bytes de la cadena. Así que el tamaño final no siempre es 1 + longitud de la cadena.

1
Cuestionario/control
Flujos de entrada y salida, nivel 36, lección 4
No disponible
Flujos de entrada y salida
Lectura y escritura de archivos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION