1. Introduction
En bref : un format binaire de sérialisation transforme un objet en une séquence d'octets qui encode sa structure et ses valeurs de la façon la plus compacte possible. Imagine que tu ne décris pas l'objet avec des mots (comme en JSON ou XML), mais que tu écris chaque bit exactement comme il est stocké en mémoire.
Dans un format textuel, les données — c'est comme une lettre à un ami en russe (chaque caractère est lisible par un humain). Dans le format binaire, c'est plutôt du morse où chaque point et trait est enregistré de façon ultra-compacte, et le lire "à la main" est presque impossible.
Schéma : comparaison des formats
| Format | Lisible par un humain | Taille du fichier | Vitesse (écriture/lecture) | Compatibilité |
|---|---|---|---|---|
| XML/JSON | Oui | Grande | Plus lent | Bonne |
| Binaire | Non | Petite | Très rapide | Limitée |
Comment fonctionne la sérialisation binaire dans .NET ?
Dans l'écosystème .NET il y a eu historiquement l'outil principal pour la sérialisation binaire — la classe BinaryFormatter. Mais avec l'évolution de la plateforme elle a été jugée dangereuse et retirée de .NET 9. Aujourd'hui les approches standards sont d'autres : BinaryWriter/BinaryReader, et pour les objets complexes — des bibliothèques tierces (par exemple, protobuf-net).
Un mot sur l'ancien (rétrospective)
BinaryFormatter savait prendre n'importe quelle classe marquée avec l'attribut [Serializable] et la transformer en octets, puis reconstruire la structure à la désérialisation. Ça avait l'air magique, mais ça cachait plein de problèmes (on en parle plus bas).
Moyens modernes
Pour les types primitifs et les structures simples il est pratique d'utiliser les classes BinaryWriter et BinaryReader. Pour les objets complexes — des bibliothèques tierces (par exemple, protobuf-net, MessagePack-CSharp, etc.).
2. Sérialisation des primitifs avec BinaryWriter
On continue à améliorer notre application pédagogique. Par exemple, on veut enregistrer les paramètres utilisateur (nom d'utilisateur, nombre de points, heure de connexion) dans un fichier binaire.
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)
{
// On ouvre le fichier pour écrire
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new BinaryWriter(stream);
// On écrit les données en morceaux. D'abord la chaîne, puis l'entier, puis la date
writer.Write(profile.Name ?? string.Empty); // chaîne
writer.Write(profile.Score); // entier
writer.Write(profile.LoginTime.ToBinary()); // la date est convertie en long
}
Voici maintenant un exemple de lecture :
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 };
}
Particularités de la sérialisation binaire primitive
Avec BinaryWriter on sérialise chaque propriété séparément. C'est une méthode fiable et prédictible : si la structure de données change, on le voit dans le code.
3. Problèmes de la sérialisation binaire classique
Regardons maintenant l'envers du décor. Pourquoi Microsoft a-t-elle été si virulente contre BinaryFormatter et a même interdit son usage ?
Fragilité du format (Schema Evolution Hell)
Les données binaires sont fortement liées à la structure de la classe. Tu changes la classe (renommage d'un champ, ajout d'un nouveau, suppression d'un ancien) — les anciens fichiers binaires deviennent illisibles. Changer l'ordre des propriétés — pareil, problème.
Illustration :
// Hier
public class Profile
{
public string Name;
public int Score;
}
// Aujourd'hui
public class Profile
{
public string Name;
public double Rating; // Nouveau champ ajouté
public int Score;
}
La lecture d'un ancien fichier va soit lever une erreur, soit lire des champs avec des données "mélangées". À la différence de JSON ou XML, où les éléments manquants peuvent être ignorés, le format binaire ne s'adapte pas — c'est comme une route en béton : tu t'en écartes un peu et ça peut tout faire tomber.
Vulnérabilités de désérialisation
Le plus gros problème de BinaryFormatter est la vulnérabilité potentielle. Si ton logiciel désérialise des données binaires venant d'une source non fiable (par ex. un utilisateur via internet), un attaquant peut y injecter un "objet" malveillant. Par le passé ça a même permis l'exécution de code arbitraire à distance sur la machine de la victime.
Cross-plateforme et compatibilité
Le sérialiseur binaire est fortement dépendant de la représentation interne des données dans .NET et de la version de l'exécution, du compilateur et de l'architecture (par ex. x64/ARM). Si tu sérialises sur Windows et essaies de désérialiser sur Linux — prépare-toi à des surprises ! Même entre versions de .NET il peut y avoir des incompatibilités.
Difficulté du diagnostic
Quand il y a un problème avec des formats textuels, on peut ouvrir le fichier, regarder le contenu et deviner ce qui ne va pas. Un fichier binaire, c'est une énigme scellée. Tout ce que tu vois, c'est un flot d'octets incompréhensible. Analyser ça est un plaisir réservé aux courageux.
4. Sérialisation binaire d'objets complexes
Références
BinaryFormatter pouvait mémoriser les liens entre objets (par ex. si deux propriétés pointent vers le même objet), mais BinaryWriter et la plupart des bibliothèques tierces n'ont pas cette magie. En général la sérialisation se fait sur le principe "j'ai imbriqué un objet dans un autre et je les écris l'un après l'autre".
Références cycliques
La sérialisation d'objets avec des références cycliques (par ex. la "maman" a une propriété Child et l'"enfant" a une propriété Parent pointant vers le parent) va soit lever une erreur, soit conduire à une boucle infinie.
Exemple :
public class Node
{
public Node? Next { get; set; }
public Node? Prev { get; set; }
}
Tenter de sérialiser cet objet "naïvement" conduira à une récursion infinie.
5. Sérialisation binaire et portabilité
Tout format binaire (surtout fait maison) est un format "pour les tiens". Si tu veux échanger des données avec d'autres programmes ou les conserver "pour longtemps" — choisis des standards ouverts : JSON, XML ou ProtoBuf.
Quand la sérialisation binaire est-elle justifiée ?
- Si les données restent dans le cadre d'une seule application et sont stockées "pour peu de temps".
- Si la vitesse et la compacité sont critiques (par ex. pour des gros logs ou de l'échange entre services dans une même écosystème).
- Si tu contrôles très précisément les deux côtés : sérialisation et désérialisation.
Alternatives : protobuf, MessagePack et autres
- protobuf-net : port de Google Protocol Buffers pour .NET, adapté aux échanges cross-platform et à la compatibilité.
- MessagePack-CSharp : implémentation rapide de MessagePack pour .NET.
Contrairement au "brut" BinaryWriter, ces bibliothèques implémentent des schémas, supportent l'évolution du format, la portabilité et la sécurité. Utilise-les si tu prévois un minimum de compatibilité avec d'autres systèmes.
6. Sérialisation binaire "manuelle"
Si tu dois quand même écrire des données binaires (par ex. pour des applis où la perf est critique), utilise BinaryWriter/BinaryReader — et toujours code explicitement l'ordre et le type des données.
Conseils :
- Écris toujours les données dans le même ordre que tu comptes les lire.
- Quand tu changes la structure du fichier, garde un numéro de version ou écris un "magic header" (Magic Header).
- Ajoute la longueur des chaînes/tableaux avant d'écrire les données elles‑mêmes.
- Documente la structure du fichier : sinon dans un an tu ne comprendras plus ton format.
Exemple : versionnage
// On enregistre le numéro de version du format en premier
writer.Write((byte)1); // Version 1
writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());
/*
Ça permet, si on change le format plus tard, d'ajouter des conditions de lecture
*/
7. Erreurs typiques quand on travaille avec la sérialisation binaire
Vous avez encodé les champs dans un ordre, et à la lecture vous les avez inversés. Résultat : les valeurs "décalent" : une chaîne est lue comme un int, un int — comme une date, etc.
Vous avez écrit 10 objets, et vous en lisez 11. Le flux est cassé : une exception de fin de fichier apparaîtra.
Vous avez changé la structure de la classe, et les anciens fichiers binaires sont illisibles — toute l'historique est perdu.
Vous avez oublié de gérer les exceptions lors de la lecture d'un fichier important — l'application plante au premier dysfonctionnement du disque (par ex. EndOfStreamException).
Vous essayez d'échanger des fichiers binaires entre différents langages sans format explicite — dans 99% des cas c'est la douleur assurée.
Vous désérialisez des données reçues par le réseau d'utilisateurs inconnus — bonjour les vulnérabilités ! N'utilisez jamais BinaryFormatter ; validez l'entrée et utilisez des formats sûrs.
GO TO FULL VERSION