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.6789ocupa 10 bytes (un byte por carácter). Además, hay que parsearlo (convertir texto a número), lo cual consume CPU. - En Binario: Un
doublesiempre 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); ...
}
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
}
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})");
}
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:
- Calcula la longitud del texto.
- Escribe la longitud al principio (como un entero codificado en 7 bits).
- 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();
}
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
0x12345678se guarda como78 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);
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.
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.