1. Intro
On connaît déjà les flux (Stream), qui sont une abstraction pour lire ou écrire des données séquentiellement. On a bossé avec FileStream pour accéder aux fichiers au niveau des octets, et aussi utilisé StreamReader et StreamWriter pour manipuler du texte facilement, qui sous le capot utilisent FileStream et gèrent les encodages.
Mais si on veut stocker dans un fichier pas juste du texte, mais des données typées : des entiers (int), des nombres à virgule (double, float), des booléens (bool), des dates (DateTime) ou même des structs custom ? Bien sûr, on pourrait tout convertir en string et écrire avec StreamWriter, puis parser à la lecture. Mais ce plan a des gros défauts :
- Inefficacité du stockage : Le nombre 12345 écrit en texte prend 5 octets (caractères). En binaire, un int ne prend que 4 octets. Sur de gros volumes, la diff est énorme.
- Performance : Les conversions constantes nombre <-> string, ça bouffe du CPU pour rien.
- Précision des données : Convertir des floats en texte puis les relire peut faire perdre de la précision à cause des arrondis.
- Parsing galère : Décomposer manuellement des strings pour extraire différents types (genre "123,45 TRUE 2024-06-21") complique le code et le rend fragile.
Pour gérer ça, il existe les classes BinaryReader et BinaryWriter. Ces classes sont des adaptateurs spécialisés qui bossent au-dessus de n’importe quel Stream de base (souvent FileStream) et offrent des méthodes pratiques pour lire/écrire les types primitifs C# en binaire. Elles s’occupent de convertir les octets en types concrets et inversement, ce qui simplifie grave la gestion des fichiers binaires structurés.
L’idée clé : BinaryReader et BinaryWriter ne sont pas des flux à part entière. Ils améliorent un Stream existant en ajoutant des méthodes pour manipuler les types C# au lieu des octets bruts.
2. Écriture de données avec BinaryWriter
BinaryWriter propose une série de méthodes Write(), surchargées pour chaque type primitif C#. Quand tu appelles une de ces méthodes, BinaryWriter convertit la valeur en binaire (suite d’octets) et écrit ces octets dans le Stream de base.
Exemple : On sauvegarde les paramètres du jeu
Imaginons qu’on veuille sauvegarder les paramètres du jeu : volume (float), niveau du joueur (int), musique activée (bool) et difficulté choisie (string).
string filePath = "settings.bin";
// 1. On crée un FileStream pour écrire
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. On crée un BinaryWriter au-dessus du FileStream, on précise l’encodage pour les strings (si besoin)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. On écrit différents types de données
writer.Write(0.75f); // float (4 octets)
writer.Write(15); // int (4 octets)
writer.Write(true); // bool (1 octet)
writer.Write("Easy"); // string (préfixe longueur + octets)
Console.WriteLine($"Paramètres sauvegardés dans '{filePath}'.");
Décryptage de l’exemple :
- On crée un FileStream avec FileMode.Create, ce qui crée un nouveau fichier ou écrase l’existant.
- Puis on crée un BinaryWriter en lui passant fs. Important : BinaryWriter ferme le flux de base (fs) quand tu appelles Dispose() (automatique avec le bloc using).
- Les méthodes writer.Write() sont intuitives : Write(float), Write(int), Write(bool), Write(string). Elles savent combien d’octets écrire pour chaque type et comment les formater.
- Pour les strings, BinaryWriter ajoute automatiquement un préfixe de longueur avant les octets de la string. Ça permet à BinaryReader de savoir combien d’octets lire pour reconstituer la string.
- Si tu ouvres settings.bin dans un éditeur texte, tu verras du "bazar", car c’est un fichier binaire. Pour voir le contenu, il faut un éditeur HEX.
3. Lecture de données avec BinaryReader
BinaryReader propose des méthodes ReadXxx() (genre ReadInt32(), ReadBoolean(), ReadString()), qui lisent le bon nombre d’octets dans le Stream de base et les convertissent dans le type C# voulu.
Exemple : On charge les paramètres du jeu
Maintenant, on va lire les paramètres depuis le fichier settings.bin qu’on a créé juste avant.
string filePath = "settings.bin";
// 1. On crée un FileStream pour lire
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. On crée un BinaryReader au-dessus du FileStream, même encodage qu’à l’écriture
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);
// 3. On lit les données DANS LE MÊME ORDRE qu’elles ont été écrites
float volume = reader.ReadSingle(); // float
int level = reader.ReadInt32(); // int
bool isMusicOn = reader.ReadBoolean(); // bool
string difficulty = reader.ReadString(); // string
Console.WriteLine($"Paramètres chargés depuis '{filePath}' :");
Console.WriteLine($"- Volume : {volume:P0}"); // Format en pourcentage (string formatting)
Console.WriteLine($"- Niveau du joueur : {level}");
Console.WriteLine($"- Musique activée : {isMusicOn}");
Console.WriteLine($"- Difficulté : {difficulty}");
Décryptage de l’exemple :
- On ouvre un FileStream en mode FileMode.Open pour lire.
- On crée un BinaryReader au-dessus de fs, avec le même encodage qu’à l’écriture.
- Super important : L’ordre des appels reader.ReadXxx() doit ABSOLUMENT correspondre à l’ordre d’écriture avec BinaryWriter. Si tu tentes de lire une string là où t’as écrit un int, tu vas te prendre une EndOfStreamException (si la string est plus longue) ou lire n’importe quoi.
- Les méthodes ReadXxx() lisent automatiquement le bon nombre d’octets et les convertissent dans le type demandé. ReadString() utilise le préfixe de longueur écrit par BinaryWriter pour savoir combien d’octets lire pour la string complète.
4. Points importants et best practices
Ordre strict :
C’est LA règle. BinaryReader et BinaryWriter ne stockent pas de métadonnées sur les types ; ils savent juste combien d’octets prend chaque type primitif. À toi de garantir le bon ordre.
Gestion des ressources (using) :
Comme la plupart des classes .NET qui manipulent des ressources système (fichiers, connexions réseau…), BinaryReader et BinaryWriter implémentent IDisposable. Donc, mets-les toujours dans un bloc using — ça garantit que Dispose() sera appelé même en cas d’erreur. Ça t’évite les fuites et ferme bien le fichier.
D’ailleurs, par défaut, BinaryWriter et BinaryReader appellent aussi Dispose() sur le flux de base (genre FileStream), donc il sera fermé automatiquement.
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... taf
Encodage pour les strings :
Pour que ça marche bien avec les strings passées à BinaryWriter.Write(string) et lues avec BinaryReader.ReadString(), mets toujours le même encodage dans leurs constructeurs (genre Encoding.UTF8). Sinon, tu risques d’avoir des soucis avec les caractères non-ASCII.
Gestion des exceptions :
Les opérations de lecture/écriture de fichiers peuvent toujours foirer (fichier absent, pas les droits, disque plein…). Mets toujours le code avec FileStream et BinaryReader/BinaryWriter dans des blocs try-catch pour assurer la fiabilité.
BaseStream et position :
Tu peux accéder au flux de base via la propriété BaseStream (genre reader.BaseStream ou writer.BaseStream). Pratique si tu veux connaître la position actuelle (BaseStream.Position) ou te déplacer dans le fichier (BaseStream.Seek()).
// Exemple d’utilisation de BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
writer.Write(123);
Console.WriteLine($"Position actuelle dans le flux : {writer.BaseStream.Position}"); // Affiche 4 (taille d’un int)
writer.Write("Hello");
Console.WriteLine($"Position actuelle dans le flux : {writer.BaseStream.Position}"); // Affiche 4 + (1+5) = 10
⚠️ La méthode Write(string) écrit d’abord la longueur de la string comme un entier 7 bits, puis les octets de la string. Donc la taille finale n’est pas toujours 1 + longueur de la string.
GO TO FULL VERSION