CodeGym /Corsi /C# SELF /Più a fondo nel formato binario e i suoi problemi

Più a fondo nel formato binario e i suoi problemi

C# SELF
Livello 43 , Lezione 3
Disponibile

1. Introduzione

In breve: il formato binario di serializzazione trasforma un oggetto in una sequenza di byte che codificano in modo il più compatto possibile la sua struttura e i suoi valori. Immagina che non descrivi l'oggetto a parole (come in JSON o XML), ma scrivi ogni suo bit esattamente come è memorizzato in memoria.

Nel formato testuale i dati sono come una lettera a un amico in russo (ogni carattere è comprensibile a una persona). Nel formato binario — è più come il codice Morse, dove ogni punto e trattino è registrato il più brevemente possibile, e leggerlo "a mano" è praticamente impossibile.

Schema: confronto dei formati

Formato Leggibile dall'essere umano Dimensione del file Velocità (scrittura/lettura) Compatibilità
XML/JSON Grande Più lento Buona
Binario No Piccolo Molto veloce Limitata

Come funziona la serializzazione binaria in .NET?

Nell'ecosistema .NET storicamente lo strumento principale per la serializzazione binaria era la classe BinaryFormatter. Ma con l'evoluzione della piattaforma è stata considerata insicura ed è stata rimossa da .NET 9. Oggi lo standard sono altri metodi: BinaryWriter/BinaryReader, e per oggetti complessi librerie esterne (per esempio, protobuf-net).

Breve storia del passato (escursione storica)

BinaryFormatter poteva prendere qualsiasi classe marcata con l'attributo [Serializable] e trasformarla in byte, e durante la deserializzazione ripristinare la struttura dell'oggetto. Suona magico, ma nascondeva molti problemi (di cui parleremo sotto).

Strumenti moderni

Per tipi primitivi e strutture semplici è comodo usare le classi BinaryWriter e BinaryReader. Per oggetti complessi — librerie esterne (per esempio, protobuf-net, MessagePack-CSharp ecc.).

2. Serializzazione di primitivi con BinaryWriter

Continuiamo a sviluppare la nostra applicazione didattica. Per esempio, vogliamo salvare le impostazioni dell'utente (nome utente, punteggio, ora di accesso) in un file 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)
{
    // Apriamo il file per la scrittura
    using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new BinaryWriter(stream);

    // Scriviamo i dati a pezzi. Prima la stringa, poi il numero, poi la data
    writer.Write(profile.Name ?? string.Empty); // stringa
    writer.Write(profile.Score);                // intero
    writer.Write(profile.LoginTime.ToBinary()); // la data viene convertita in "long"
}

Ora un esempio di lettura:

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 };
}

Particolarità della serializzazione binaria dei primitivi

Con BinaryWriter serializziamo ogni proprietà separatamente. È un modo affidabile e predicibile: se la struttura dei dati cambia, lo vediamo nel codice.

3. Problemi della serializzazione binaria classica

Ora vediamo l'altra faccia della medaglia. Perché, in effetti, Microsoft criticava così duramente BinaryFormatter e ne vietava l'uso?

Fragilità del formato (Schema Evolution Hell)

I dati binari sono strettamente legati alla struttura della classe. Hai cambiato la classe (hai rinominato un campo, aggiunto uno nuovo, rimosso uno vecchio) — i vecchi file binari diventano illeggibili. Hai cambiato l'ordine delle proprietà — anche quello è un problema.

Illustrazione:

// Ieri
public class Profile
{
    public string Name;
    public int Score;
}

// Oggi
public class Profile
{
    public string Name;
    public double Rating; // È stato aggiunto un nuovo campo
    public int Score;
}

La lettura di un file vecchio genererà o un errore, o leggerà i campi con dati "mischiati". A differenza di JSON o XML, dove gli elementi mancanti si possono saltare, il formato binario non si adatta ai cambiamenti — è come una strada di cemento compatto: se esci leggermente dalla traiettoria — e subito "crolli dalla bicicletta".

Vulnerabilità durante la deserializzazione

Il problema più grande di BinaryFormatter è la potenziale vulnerabilità. Se il tuo software deserializza dati binari provenienti da una fonte non affidabile (per esempio, un utente su Internet), un attaccante può infilare un "oggetto" malevolo. In passato questo ha portato anche all'esecuzione remota di codice arbitrario sulla macchina della vittima.

Cross-platform e compatibilità

Un serializzatore binario è strettamente legato alla rappresentazione interna dei dati in .NET e alla versione dell'ambiente di runtime, del compilatore e dell'architettura (per esempio, x64/ARM). Se serializzi su Windows e provi a deserializzare su Linux — le sorprese sono assicurate! Anche tra versioni di .NET possono emergere incompatibilità.

Difficoltà di diagnostica

Con i formati testuali, quando qualcosa va storto puoi aprire il file, guardare il contenuto e intuire cosa non va. Un file binario è un enigma a sette sigilli. Tutto quello che vedono i tuoi occhi è un flusso di byte senza senso. "Analizzare" quel file è un passatempo per coraggiosi.

4. Serializzazione binaria di oggetti complessi

Riferimenti

BinaryFormatter ricordava i legami tra oggetti (per esempio, se due proprietà puntavano allo stesso oggetto), ma BinaryWriter e la maggior parte delle librerie esterne non hanno questa magia. Di solito la serializzazione avviene con il principio "incapsula un oggetto dentro un altro e scrivili in sequenza".

Riferimenti ciclici

Serializzare oggetti con riferimenti ciclici (per esempio, la "mamma" ha la proprietà Child, e il "bambino" ha la proprietà Parent che punta al genitore) o causa un errore, o porta a un ciclo infinito.

Esempio:

public class Node
{
    public Node? Next { get; set; }
    public Node? Prev { get; set; }
}

Il tentativo di serializzare questo oggetto "in modo ingenuo" porterà al loop.

5. Serializzazione binaria e portabilità

Qualsiasi formato binario (soprattutto homemade) è un formato "solo per i nostri". Se prevedi di scambiare dati con altri programmi o conservarli "per sempre" — scegli standard aperti: JSON, XML o ProtoBuf.

Quando ha senso la serializzazione binaria?

  • Se i dati vivono all'interno della stessa applicazione e sono salvati "per un periodo breve".
  • Se la velocità e la compattezza sono importanti (per esempio, per grandi log o scambio tra servizi all'interno della stessa ecosistema).
  • Se controlli molto rigorosamente entrambe le parti: sia la serializzazione che la deserializzazione.

Alternative: protobuf, MessagePack e altre

  • protobuf-net: port di Google Protocol Buffers per .NET, adatto per scambi cross-platform e per la compatibilità.
  • MessagePack-CSharp: implementazione veloce di MessagePack per .NET.

A differenza del "puro" BinaryWriter, queste librerie implementano schemi, supportano l'evoluzione del formato, la cross-platform e la sicurezza. Usale se prevedi anche solo una minima compatibilità con altri sistemi.

6. Serializzazione binaria "manuale"

Se hai davvero bisogno di scrivere dati binari (per esempio, in applicazioni dove conta la performance), usa BinaryWriter/BinaryReader — e sempre codifica esplicitamente l'ordine e i tipi dei dati.

Consigli:

  • Scrivi sempre i dati nello stesso ordine con cui prevedi di leggerli.
  • Quando cambi la struttura del file, mantieni un numero di versione o scrivi un "magic header" (Magic Header).
  • Aggiungi la lunghezza di stringhe/array prima di scrivere i dati stessi.
  • Documenta la struttura del file: altrimenti dopo un anno non capirai più il tuo formato.

Esempio: versioning

// Salviamo il numero di versione del formato al primo posto
writer.Write((byte)1); // Versione 1

writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());

/*
Permette, al cambiamento del formato in futuro, di aggiungere condizioni di lettura
*/

7. Errori tipici nel lavoro con la serializzazione binaria

Hai codificato i campi in un ordine, e durante la lettura li hai scambiati. Di conseguenza i valori "si spostano": una stringa viene letta come int, un int — come data ecc.

Hai scritto 10 oggetti, ma ne leggi 11. Il flusso viene violato: si genererà un'eccezione di fine file.

Hai cambiato la struttura della classe, e i vecchi file binari non si possono leggere — perdi tutta la storia dei dati.

Hai dimenticato di gestire le eccezioni durante la lettura di un file importante — l'applicazione crasha al primo problema su disco (per esempio, EndOfStreamException).

Si cerca di scambiare file binari tra linguaggi diversi senza un formato esplicito — nel 99% dei casi è dolore garantito.

Deserializzi dati ricevuti in rete da utenti sconosciuti — ciao vulnerabilità! Non usare mai BinaryFormatter; valida l'input e usa formati sicuri.

2
Compito
C# SELF, livello 43, lezione 3
Bloccato
Scrittura dei dati dell'utente in un file binario
Scrittura dei dati dell'utente in un file binario
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION