1. Introducción
En resumen: el formato binario de serialización convierte un objeto en una secuencia de bytes que codifican su estructura y valores de la forma más compacta posible. Imagina que no sólo describes el objeto con palabras (como en JSON o XML), sino que escribes cada bit tal y como se guarda en memoria.
En formato de texto los datos son como una carta a un amigo en ruso (cada carácter es comprensible por una persona). En formato binario es más bien código Morse, donde cada punto y raya se registra de la forma más corta y leerlo "a mano" es imposible.
Esquema: comparación de formatos
| Formato | Legible por humanos | Tamaño del archivo | Velocidad (escritura/lectura) | Compatibilidad |
|---|---|---|---|---|
| XML/JSON | Sí | Grande | Más lento | Buena |
| Binario | No | Pequeño | Muy rápido | Limitada |
¿Cómo funciona la serialización binaria en .NET?
En el ecosistema .NET históricamente hubo una herramienta principal para la serialización binaria — la clase BinaryFormatter. Pero con la evolución de la plataforma se la consideró insegura y fue eliminada de .NET 9. Actualmente los estándares son otros: BinaryWriter/BinaryReader, y para objetos complejos — librerías externas (por ejemplo, protobuf-net).
Brevísimo repaso histórico
BinaryFormatter podía tomar cualquier clase marcada con el atributo [Serializable] y convertirla en bytes, y al deserializar reconstruir la estructura del objeto. Suena mágico, pero ahí residían muchos problemas (de eso hablaremos más abajo).
Herramientas modernas
Para tipos primitivos y estructuras simples es cómodo usar las clases BinaryWriter y BinaryReader. Para objetos complejos hay librerías de terceros (por ejemplo, protobuf-net, MessagePack-CSharp, etc.).
2. Serialización de primitivos con BinaryWriter
Sigamos mejorando nuestra aplicación de estudio. Por ejemplo, queremos guardar la configuración del usuario (nombre, puntuación, tiempo de inicio de sesión) en un archivo binario.
public class UserProfile
{
public string Name { get; set; }
public int Score { get; set; }
public DateTime LoginTime { get; set; }
}
public static Task SaveUserProfile(UserProfile profile, string filePath)
{
// Abrimos el archivo para escritura
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new BinaryWriter(stream);
// Escribimos los datos por partes. Primero la cadena, luego el número, luego la fecha
writer.Write(profile.Name ?? string.Empty); // cadena
writer.Write(profile.Score); // entero
writer.Write(profile.LoginTime.ToBinary()); // la fecha se convierte a "long"
}
Ahora un ejemplo de lectura:
public static Task<UserProfile> LoadUserProfile(string filePath)
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(stream);
string name = reader.ReadString();
int score = reader.ReadInt32();
long dateData = reader.ReadInt64();
DateTime loginTime = DateTime.FromBinary(dateData);
return new UserProfile { Name = name, Score = score, LoginTime = loginTime };
}
Particularidades de la serialización binaria de primitivos
Con BinaryWriter serializamos cada propiedad por separado. Es un método fiable y predecible: si la estructura de datos cambia, se nota en el código.
3. Problemas de la serialización binaria clásica
Veamos ahora la otra cara de la moneda. ¿Por qué Microsoft criticó tan fuertemente BinaryFormatter e incluso prohibió su uso?
Fragilidad del esquema (Schema Evolution Hell)
Los datos binarios están fuertemente ligados a la estructura de la clase. Cambiaste la clase (renombraste un campo, añadiste uno nuevo, eliminaste otro) — los archivos binarios antiguos dejan de ser legibles. Cambiaste el orden de las propiedades — también es un problema.
Ilustración:
// Ayer
public class Profile
{
public string Name;
public int Score;
}
// Hoy
public class Profile
{
public string Name;
public double Rating; // Se añadió un nuevo campo
public int Score;
}
Leer un archivo antiguo provocará o bien un error, o bien que los campos se interpreten con "datos mezclados". A diferencia de JSON o XML, donde los elementos faltantes se pueden omitir, el formato binario no se adapta a los cambios — es como una carretera de hormigón: te sales un poco y la bicicleta se cae.
Vulnerabilidades de deserialización
El mayor problema de BinaryFormatter es la potencial vulnerabilidad. Si tu software deserializa datos binarios recibidos de una fuente no confiable (por ejemplo, un usuario por Internet), un atacante puede inyectar un "objeto" malicioso. En el pasado esto llegó incluso a ejecución remota de código en la máquina víctima.
Portabilidad y compatibilidad
Un serializador binario está muy ligado a la representación interna de los datos en .NET y a la versión del runtime, del compilador y la arquitectura (por ejemplo, x64/ARM). Si serializas en Windows y tratas de deserializar en Linux — ¡sorpresas garantizadas! Incluso entre versiones de .NET pueden surgir incompatibilidades.
Incomodidad para diagnosticar
Con formatos de texto, si surge un problema puedes abrir el archivo, mirar el contenido y deducir qué falló. Un archivo binario es un misterio sellado. Lo único que tus ojos ven es un flujo de bytes sin sentido. "Analizar" ese archivo es un hobby para valientes.
4. Serialización binaria de objetos complejos
Referencias
BinaryFormatter recordaba las relaciones entre objetos (por ejemplo, si dos propiedades apuntaban al mismo objeto), pero BinaryWriter y la mayoría de librerías externas no hacen esa magia. Normalmente la serialización es por composición: incluido un objeto dentro de otro y escritos en orden.
Referencias cíclicas
Serializar objetos con referencias cíclicas (por ejemplo, la "madre" tiene la propiedad Child, y el "hijo" tiene la propiedad Parent que apunta al padre) o bien lanza un error, o bien causa un bucle infinito.
Ejemplo:
public class Node
{
public Node? Next { get; set; }
public Node? Prev { get; set; }
}
Intentar serializar este objeto "de forma naïve" llevará a un ciclo infinito.
5. Serialización binaria y portabilidad
Cualquier formato binario (especialmente uno hecho a mano) es un formato "solo para nosotros". Si vas a intercambiar datos con otros programas o guardarlos "por décadas" — elige estándares abiertos: JSON, XML o ProtoBuf.
¿Cuándo tiene sentido la serialización binaria?
- Si los datos viven dentro de una sola aplicación y se guardan "a corto plazo".
- Si la velocidad y la compacidad son importantes (por ejemplo, para logs grandes o intercambio entre servicios dentro de la misma ecosistema).
- Si controlas estrictamente ambos extremos: tanto la serialización como la deserialización.
Alternativas: protobuf, MessagePack y otras
- protobuf-net: puerto de Google Protocol Buffers para .NET, adecuado para intercambios multiplataforma y compatibilidad.
- MessagePack-CSharp: implementación rápida de MessagePack para .NET.
A diferencia del "crudo" BinaryWriter, estas librerías implementan esquemas, soportan evolución del formato, portabilidad y seguridad. Úsalas si planeas alguna compatibilidad con otros sistemas.
6. Serialización binaria "manual"
Si aun así necesitas escribir datos binarios (por ejemplo, en apps donde la performance importa), usa BinaryWriter/BinaryReader — y siempre codifica explícitamente el orden y el tipo de los datos.
Consejos:
- Siempre escribe los datos en el mismo orden en que planeas leerlos.
- Al cambiar la estructura del archivo mantén un número de versión o escribe una "cabecera mágica" (Magic Header).
- Añade la longitud de cadenas/arrays antes de escribir los datos en sí.
- Documenta la estructura del archivo: si no, dentro de un año ni tú entenderás tu propio formato.
Ejemplo: versionado
// Guardamos el número de versión del formato en la primera posición
writer.Write((byte)1); // Versión 1
writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());
/*
Permite, al cambiar el formato posteriormente, añadir condiciones de lectura
*/
7. Errores típicos al trabajar con serialización binaria
Codificaste los campos en un orden y al leerlos los cambiaste. Como resultado, los valores "se descolocan": una cadena se lee como int, un int como fecha, etc.
Escribiste 10 objetos, y lees 11. Se rompe el flujo: se lanzará una excepción por fin de archivo.
Cambiaste la estructura de la clase y los archivos binarios antiguos no se pueden leer — se pierde todo el historial de datos.
Olvidaste manejar excepciones al leer un archivo importante — la aplicación se cae ante el primer fallo en disco (por ejemplo, EndOfStreamException).
Intentas intercambiar archivos binarios entre distintos lenguajes sin un formato explícito — en el 99% de los casos eso será un dolor asegurado.
Deserializas datos recibidos por la red de usuarios desconocidos — hola, vulnerabilidades. Nunca uses BinaryFormatter; valida la entrada y usa formatos seguros.
GO TO FULL VERSION