ficheros-binarios-csharp

Cómo leer y escribir ficheros binarios en C#

  • 5 min

Un fichero binario es un archivo que almacena datos como bytes siguiendo una estructura concreta.

Hasta ahora hemos hablado de ficheros de texto. Son geniales: los puedes abrir con el Bloc de notas, leerlos y entenderlos. Pero tienen un defecto importante en ingeniería: pueden ser poco eficientes.

Imagina que tienes que guardar un millón de lecturas de un sensor, representadas como números decimales (double).

  • En Texto: El número 12345.6789 ocupa 10 bytes (un byte por carácter). Además, hay que parsearlo (convertir texto a número), lo cual consume CPU.
  • En Binario: Un double siempre ocupa 8 bytes. No hay parseo de texto, porque se guarda una representación binaria fija.

Los ficheros binarios no están hechos para ser leídos por humanos, sino por máquinas. Son compactos, rápidos y estructurados.

La base: FileStream

Todo empieza con el FileStream. A diferencia de los métodos rápidos de texto, aquí necesitamos controlar el flujo de bytes con precisión.

string ruta = "datos.bin";

// FileMode.Create: Crea o sobrescribe
// FileAccess.Write: Solo escritura
using (FileStream fs = new FileStream(ruta, FileMode.Create, FileAccess.Write))
{
    // Aquí tenemos un flujo abierto listo para recibir bytes
    // fs.WriteByte(0xFF); ...
}
Copied!

Sin embargo, usar FileStream “a pelo” es doloroso porque solo entiende de byte[]. Si quieres escribir un entero (int), tendrías que trocearlo manualmente en 4 bytes. Para evitar ese sufrimiento, usamos Wrappers (envoltorios).

BinaryWriter y BinaryReader

Estas dos clases son nuestros traductores. Se encargan de coger tus tipos de datos de C# (int, float, bool, string) y convertirlos a su representación binaria cruda para escribirlos en el stream.

El orden aquí es sagrado. En un fichero binario no hay “líneas” ni etiquetas. Si escribes un entero y luego un booleano, debes saber que tienes que leerlo exactamente en ese orden.

using (FileStream fs = new FileStream("sensor.dat", FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
    int idSensor = 1;
    double temperatura = 23.5;
    bool activo = true;

    // Escribimos secuencialmente
    writer.Write(idSensor);    // 4 bytes
    writer.Write(temperatura); // 8 bytes
    writer.Write(activo);      // 1 byte
}
Copied!

Para recuperar la información, hacemos el proceso inverso.

using (FileStream fs = new FileStream("sensor.dat", FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
    // ¡EL ORDEN ES CRÍTICO!
    int id = reader.ReadInt32();
    double temp = reader.ReadDouble();
    bool status = reader.ReadBoolean();

    Console.WriteLine($"Sensor {id}: {temp}ºC (Activo: {status})");
}
Copied!

Si te equivocas en el orden (por ejemplo, intentas leer un double donde había un int), leerás 8 bytes de basura, desalinearás todo el fichero y los datos resultantes no tendrán sentido.

Estructuras y cadenas de texto

Las cadenas (string) en binario son un caso especial. Como un texto puede medir lo que sea, BinaryWriter hace algo inteligente:

  1. Calcula la longitud del texto.
  2. Escribe la longitud al principio (como un entero codificado en 7 bits).
  3. Escribe los bytes del texto.

Esto significa que reader.ReadString() sabe exactamente cuántos bytes leer.

Ojo con la interoperabilidad C++: Si este fichero lo va a leer un programa en C o C++, ellos no esperan ese prefijo de longitud de C#. Tendrás que escribir cadenas de longitud fija o manejarlo manualmente.

Acceso aleatorio (Seek)

Una de las grandes ventajas de los ficheros binarios es que suelen tener registros de tamaño fijo.

Si guardamos 1000 mediciones y cada una ocupa 12 bytes (4 de int + 8 de double), sabemos matemáticamente dónde está la medición número 500. No hace falta leer las 499 anteriores.

Podemos saltar directamente usando BaseStream.Seek.

long posicion = 500 * 12; // (Índice * TamañoRegistro)

using (FileStream fs = new FileStream("mediciones.bin", FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
    // Saltamos directamente a la posición
    reader.BaseStream.Seek(posicion, SeekOrigin.Begin);

    // Leemos solo ese registro
    int id = reader.ReadInt32();
    double valor = reader.ReadDouble();
}
Copied!

El problema del endianness

Si trabajas con hardware, microcontroladores (Arduino, STM32) o protocolos de red, te encontrarás con el endianness.

El Endianness define el orden de los bytes de un dato multibyte.

  • Little Endian (Intel/AMD, C# por defecto): El byte menos significativo va primero. El número 0x12345678 se guarda como 78 56 34 12.
  • Big Endian (Redes, algunos micros): El byte más significativo va primero. Se guarda como 12 34 56 78.

Si lees con BinaryReader un fichero generado en big-endian, los números se interpretarán mal porque este lector usa little-endian para los tipos numéricos.

Para solucionarlo, a veces tenemos que leer los bytes crudos y usar utilidades como BinaryPrimitives.

using System.Buffers.Binary;

// Lectura Big Endian para un entero de 16 bits (short)
byte[] bytes = reader.ReadBytes(2);
short valor = BinaryPrimitives.ReadInt16BigEndian(bytes);
Copied!

Serialización de estructuras con marshaling

Cuando tenemos estructuras de datos complejas en ingeniería (por ejemplo, una cabecera de un archivo BMP o un paquete TCP), leer campo a campo con writer.Write es tedioso.

Podemos volcar una struct entera a bytes directamente. Esto es técnica avanzada y requiere usar System.Runtime.InteropServices.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Cabecera
{
    public int Id;
    public float Version;
    public short Flags;
}

// Convertir struct a bytes
int size = Marshal.SizeOf(miCabecera);
byte[] arr = new byte[size];

IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
    Marshal.StructureToPtr(miCabecera, ptr, false);
    Marshal.Copy(ptr, arr, 0, size);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

// Ahora 'arr' contiene la estructura en binario puro lista para guardar.
Copied!

En versiones modernas de .NET, preferimos usar Span<T> y MemoryMarshal para hacer esto de forma mucho más rápida y segura sin usar punteros explícitos.