Serializzazione - Prima parte
a cura di Enrico Barillari (requisiti: conoscenza minima di .Net e C#)Di che si parla
Per serializzazione si intende la possibilità di salvare dati in un formato tale da permettere in seguito di recuperarli. In questo senso si può parlare di serializzazione di dati in un database, ma anche in un file binario, un file di testo, un file Xml, o addirittura un flusso in memoria.L'ambiente di sviluppo di .Net mette a disposizione diversi metodi predefiniti, per la serializzazione e la deserializzazione di dati, a partire dalla serializzazione automatica, che si può realizzare semplicemente aggiungendo l'attributo Serializable ad una classe che rispetti determinate caratteristiche, ovvero un costruttore di default, senza argomenti, e proprietà pubbliche con metodi accessori get e set.
L'attributo Serializable fa sì che tutti i campi della classe siano serializzati, se rispettano queste semplici regole, tuttavia è possibile contrassegnare un campo come non serializzabile, aggiungendo l'attributo NonSerialized, e questo campo non verrà serializzato. Per serializzare una classe occorre un formattatore, per esempio la classe BinaryFormatter, ovvero una classe che implementi l'interfaccia IFormatter, che permette di serializzare una classe in formato binario, su un flusso di dati.
Un esempio col formattatore binario
Vediamo di fare un esempio, partendo dalle classi che io e Sabrina Cosolo abbiamo definito nel nostro articolo su classi ed OOP, la classe Persona, ed aggiungiamo l'attributo Serializable ed i metodi accessori get alle proprietà in cui non erano stati definiti (vedi Listato1)Sulla base di questa classe Persona, possiamo definire una classe BinarySerializer (vedi Listato2), che sfrutta un formattatore binario (BinaryFormatter) ed un flusso (FileStream) per persistere un file binario nella directory bin\debug dell'applicazione, e successivamente rileggere i dati, e riformattarli come stringhe.
E questi sono i metodi necessari per serializzare, e successivamente recuperare i dati relativi ad una istanza della classe Persona:
private void buttonSerialize_Click(object sender, System.EventArgs e) { Persona persona = new Persona(this.txtNome.Text, this.txtCognome.Text, this.txtIndirizzo.Text,this.txtCitta.Text, this.txtDataNascita.Text); ebBinarySerializer serializer = new ebBinarySerializer(); serializer.Serialize(persona); } private void buttonDeserialize_Click(object sender, System.EventArgs e) { ebBinarySerializer serializer = new ebBinarySerializer(); Persona persona = (Persona)serializer.Deserialize(); this.txtNome.Text = persona.Nome; this.txtCognome.Text = persona.Cognome; this.txtIndirizzo.Text = persona.Indirizzo; this.txtCitta.Text = persona.Citta; this.txtDataNascita.Text = persona.DataNascita; }Nessun problema nel caso volessimo serializzare un array di persone:
Persona persona = new Persona(this.txtNome.Text, this.txtCognome.Text, this.txtIndirizzo.Text,this.txtCitta.Text, this.txtDataNascita.Text); Persona persona2 = new Persona(this.txtNome2.Text, this.txtCognome2.Text, this.txtIndirizzo2.Text,this.txtCitta2.Text, this.txtDataNascita2.Text); ebBinarySerializer serializer = new ebBinarySerializer(); Persona[] persone = new Persona[]{persona, persona2}; serializer.Serialize(persone);e deserializzare lo stesso array:
Persona[] persone = new Persona[2]; ebBinarySerializer serializer = new ebBinarySerializer(); persone = (Persona[])serializer.Deserialize(); Persona persona = persone[0]; this.txtNome.Text = persona.Nome; this.txtCognome.Text = persona.Cognome; this.txtIndirizzo.Text = persona.Indirizzo; this.txtCitta.Text = persona.Citta; this.txtDataNascita.Text = persona.DataNascita; persona = persone[1]; this.txtNome2.Text = persona.Nome; this.txtCognome2.Text = persona.Cognome; this.txtIndirizzo2.Text = persona.Indirizzo; this.txtCitta2.Text = persona.Citta; this.txtDataNascita2.Text = persona.DataNascita;Si può esplorare il file dall'interno dell'applicazione, cliccando il menu File/Open/File, e selezionando il file nella cartella bin\debug dell'applicazione.
Un esempio con il formattatore Soap
Proseguiamo l'esplorazione dei formattatori predefiniti di .Net con il formattatore Soap: per prima cosa occorre aggiungere al progetto un riferimento alla libreria System.Runtime.Serialization.Formatters.Soap, dal Solution Explorer, click destro sulla cartella References, Add Reference.La classe è praticamente identica alla precedente, eccetto per il formattatore SoapFormatter al posto di quello binario:
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Soap; namespace ebSerialization { /// <summary> /// Summary description for SoapSerializer. /// </summary> public class ebSoapSerializer { ///<summary> Variabile member: nome completo del file</summary> private string filePath = Directory.GetCurrentDirectory() + @"\PersonaSoap.dat"; ///<summary> ///Metodo: Costruttore predefinito ///</summary> public ebSoapSerializer() { } ///<summary> ///Metodo: Serializza un oggetto in formato Soap ///</summary> /// <param name="obj">Oggetto da serializzare</param> public void Serialize(object obj) { FileStream fs = null; try { // Creo un flusso per l'operazione di scrittura dei dati su file // Se il file non esiste viene creato // In questo caso devo avere accesso in modalità scrittura fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write); // Occorre un'istanza della classe SoapFormatter SoapFormatter formatter = new SoapFormatter(); // e questo é tutto ciò che serve per persistere i dati formatter.Serialize(fs, obj); } catch(Exception ex) { throw new ApplicationException("SoapFormatter.Serialize: " + ex.Message); } finally { // Chiudere sempre i flussi non appena possibile fs.Close(); } } ///<summary> ///Metodo: Deserializza un oggetto da un file in formato Soap ///</summary> public object Deserialize() { FileStream fs = null; try { // Creo un flusso per l'operazione di lettura dei dati dal file // Se il file non esiste viene generata un'eccezione // In questo caso devo avere accesso in modalità lettura fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); // Occorre un'istanza della classe SoapFormatter SoapFormatter formatter = new SoapFormatter(); // e questo é tutto ciò che serve per leggere i dati dal formato SOAP return formatter.Deserialize(fs); } catch(FileNotFoundException) { throw new ApplicationException("File inesistente"); } finally { fs.Close(); } } } }I metodi per utilizzare questa classe sono identici ai precedenti, eccetto l'istanza della classe che utilizziamo per eseguire la serializzazione:
Persona persona = new Persona(this.txtNome.Text, this.txtCognome.Text, this.txtIndirizzo.Text,this.txtCitta.Text, this.txtDataNascita.Text); Persona persona2 = new Persona(this.txtNome2.Text, this.txtCognome2.Text, this.txtIndirizzo2.Text,this.txtCitta2.Text, this.txtDataNascita2.Text); ebSoapSerializer serializer = new ebSoapSerializer(); Persona[] persone = new Persona[]{persona, persona2}; serializer.Serialize(persone);La differenza si nota piuttosto nelle dimensioni del file Soap, decisamente maggiori rispetto al file binario.
D'altra parte, il formato Soap è decisamente più adatto per lo scambio di informazioni sul web, in applicazioni che utilizzano vari protocolli, tra i quali HTTP, SMTP ed FTP.
Come si può vedere aprendo il file con l'editor di Visual Studio, oppure con un editor di testo, il file è in un formato basato su XML, racchiuso nel cosiddetto 'envelope' che definisce le caratteristiche del messaggio, e con una sezione che definisce le regole di codifica ed il tipo di dati (in questo caso un array):
(n.d.r.: le righe, che in realtà vanno da < a >, sono mandate a capo per ragioni di leggibilità, come altrove nel codice quando necessario)<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <SOAP-ENC:Array SOAP-ENC:arrayType="a1:Persona[2]" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ebSerialization/Serialization%2C %20Version%3D1.0.1766.31270%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> <item href="#ref-3"/> <item href="#ref-4"/> </SOAP-ENC:Array> <a1:Persona id="ref-3" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ebSerialization/Serialization%2C %20Version%3D1.0.1766.31270%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> <mNome id="ref-5">enrico</mNome> <mCognome id="ref-6">barillari</mCognome> <cut> </a1:Persona> </SOAP-ENV:Body> </SOAP-ENV:Envelope>Serializzazione XML
Veniamo ora alla serializzazione Xml, la più adatta al trattamento di dati in quantità tali da poter essere consumati da un'applicazione reale, in quanto più flessibile delle precedenti, anche se richiede un controllo più accurato sulle modalità di scrittura e lettura dei dati.
Va ricordato che in questo tipo di serializzazione vengono ignorati i campi con valore null, e che non vengono mantenute le informazioni sul nome della classe e sullo spazio dei nomi.
Di contro, con Xml si possono definire lo schema, lo spazio dei nomi, i nomi degli elementi, il tipo di rappresentazione dei dati, che possono essere scritti come elementi, come attributi o come testo.La classe XmlSerializer si incarica di creare uno schema XSD e gli oggetti Writer e Reader necessari a serializzare e deserializzare dati.
Possiamo definire una classe simile alle precedenti per la serializzazione XML, ma in questo caso la classe non accetterà più un oggetto generico, bensì un oggetto Persona, in quanto i costruttori della classe XmlSerializer necessitano di un tipo ben definito (vedi Listato3).I metodi per la serializzazione e la deserializzazione sono sempre i soliti:
Persona persona = new Persona(this.txtNome.Text, this.txtCognome.Text, this.txtIndirizzo.Text,this.txtCitta.Text, this.txtDataNascita.Text); ebXmlSerializer serializer = new ebXmlSerializer(); serializer.Serialize(persona); ebXmlSerializer serializer = new ebXmlSerializer(); Persona persona = (Persona)serializer.Deserialize(); this.txtNome.Text = persona.Nome; this.txtCognome.Text = persona.Cognome; this.txtIndirizzo.Text = persona.Indirizzo; this.txtCitta.Text = persona.Citta; this.txtDataNascita.Text = persona.DataNascita;Questo l'output, nel file XML:
<?xml version="1.0"?> <Persona xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Nome>Pinco</Nome> <Cognome>Pallino</Cognome> <Indirizzo>via Caio Sempronio, 1</Indirizzo> <Citta>Roma</Citta> <DataNascita>25/12/2000</DataNascita> </Persona>Come detto in precedenza, con la serializzazione XML possiamo avere un maggiore controllo sulla formattazione del file, per esempio utilizzando gli attributi di serializzazione, appartenenti allo spazio dei nomi System.Xml.Serialization.
Possiamo decidere di scrivere una proprietà della nostra classe come attributo piuttosto che come elemento, come abbiamo fatto in precedenza, contrassegnandola con l'attributo XmlAttributeAttribute.///<summary> ///Property: Espone il nome della persona ///</summary> [XmlAttributeAttribute(AttributeName="Nome")] public string Nome { } ///<summary> ///Property: Espone il cognome della persona ///</summary> [XmlAttributeAttribute(AttributeName="Cognome")] public string Cognome { }In questo modo l'output su file diventerà questo:
<?xml version="1.0"?> <Persona xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Nome="Pinco" Cognome="Pallino"> <Indirizzo>via Tizio Caio, 1</Indirizzo> <Citta>Roma</Citta> <DataNascita>25/12/2000</DataNascita> </Persona>Le proprietà Nome e Cognome, che prima erano rappresentate come elementi, ora sono diventate attributi del nodo radice Persona.
Attributi
I principali attributi di serializzazione sono:
- XmlRoot, i cui parametri permettono di definire un nome, uno spazio dei nomi, un tipo di dati; XmlElement che accetta gli stessi parametri del precedente, oltre ad un parametro IsNullable che definisce il comportamento in caso di proprietà con valore null: nel caso in cui abbia valore true viene serializzato un elemento vuoto con un attributo xsi:nil; nel caso abbia valore false la proprietà viene ignorata;
- XmlAttribute permette di contrassegnare campi che devono essere serializzati come attributi;
- XmlArray indica che la proprietà verrà serializzata come matrice di elementi
- XmlArrayItem definisce il nome ed il tipo di dati di ogni singolo elemento appartenente all'array
- XmlText consente la rappresentazione di una proprietà come semplice testo XML
- XmlIgnore indica al serializzatore di ignorare la proprietà, che non viene così serializzata
(segue)