Serializzazione in C#

In questo articolo illustro le basi della serializzazione in C Sharp

La serializzazione è quel processo per cui si vuole che delle istanze di un determinato Tipo possano essere trasformate in oggetti manipolabili dalla CRL mediante strutture generiche come stringhe e file. Ad esempio, consideriamo come tipo serializzabile la classe “Persona” una sua istanza, ovvero un oggetto di questo tipo, può esser serializzata per i scopi più vari, ad esempio:

  1. La trasmissione di una istanza ad un server o ad un client sulla rete
  2. La persistenza di una istanza su file system
    e così via…

La serializzazione avviene considerando un set di componenti base da utilizzare i quali sono : un serializzatore e una fonte di input dei dati. La fonte dei dati può essere: uno stream o una stringa. Il  serializzatore ci permette di definire il tipo di serializzazione che vogliamo usare nel nostro software: i tipi più famosi di serializzazione sono Binario, XML e JSON.

In C# abbiamo due “modi di pensare” della serializzazione, in sostanza, si può decidere di definire un contratto chiamato DataContract oppure di serializzare integralmente l’oggetto, annotando solo le variazioni richieste.

DataContract

La classe DataContract permette di serializzare le classi mediante “un contratto” da definire all’interno delle stesse.
La definizione delle classi avviene mediante annotazioni, indipendentemente dalla visibilità degli elementi annotati, in particolare si usano:

  1. L’annotazione DataContract, che definisce il contratto di serializzazione dell’oggetto in elementi atomici
  2. L’annotazione DataMember che definisce un elemento della serializzazione.

A seguire un esempio di classe annotata mediante DataContract.

[csharp]
[DataContract(Name = "Persona", Namespace = "http://www.persona.com")]
class Persona
{
[DataMember(Order =1, Name ="NomeDiBattesimo")]
public string nome {
get { return _nome; }
set { _nome = value; }
}
private string _nome;

[DataMember(Order = 2,IsRequired = false,Name ="Cognome", EmitDefaultValue = true)]
public string cognome
{
get;
set;
}
[/csharp]

L’attributo DataContract può avere le seguenti opzioni:

  • Name : Attribuisce un nome la contratto utilizzato, risulta utile per evitare conflitti di nomi.
  • NameSpace : Attribuisce il namespace da utilizzare, risulta utile nei file XML.
  • IsReference : Permette di istruire il serializzatore affinché vengano gestiti i riferimenti circolari all’interno del file XML.

Considera, ad esempio, che l’oggetto persona abbia il riferimento ad un elenco di persone associate: in questo caso due persone (ad esempio dei figli) possono essere associati ad una sola persona (il padre) e pertanto verrà visualizzato un attributo nel file XML risultante che collega il figlio con il suo padre.

L’attributo DataMember può avere le seguenti opzioni:

  • Order : Permette di definire l’ordine di apparizione rispetto al padre del nodo, nel nostro esempio, nome apparirà prima di cognome.
  • Name : Attribuisce l’etichetta visualizzata dal membro.
  • EmitDefaultValue : permette di utilizzare comunque il membro anche se non valorizzato o presente, considerando il valore di default.
  • IsRequired : se il campo non è valorizzato in fase di deserializzazione (lettura dalla sorgente), viene lanciata un’eccezione. In fase di serializzazione l’utilizzo dev’essere in accordo con l’opzione EmitDefaultValue: ovvero se il campo è obbligatorio (IsRequired è true) allora dev’essere valorizzato nell’oggetto oppure EmitDefaultValue dev’essere true.

Con il DataContract possiamo ottenere la serializzazione in due differenti tipologie di formato: XML e JSON ottenibili rispettivamente con le classi DataContractSerializerDataContractJsonSerializer.

Entrambe le classi sono specializzazioni della classe astratta XmlObjectSerializer, per entrambe le classi bisogna passare l’oggetto Type relativo all’oggetto da serializzare.

[csharp]
DataContractSerializer dcs = new DataContractSerializer(typeof(Persona));
Persona p = new Persona();
p.cognome = "CognomePersona";
p.nome = "NomePersona";
p.Indirizzo = "via dei martiri 28";

FileStream fs = File.OpenWrite("serializzato.xml");
dcs.WriteObject(fs,p);
fs.Flush();
fs.Close();
[/csharp]

In questo caso abbiamo scelto di scrivere su file system l’oggetto persona, leggendo il file mediante File.OpenRead
e utilizzando il metodo ReadObject possiamo leggere il file. Osserva che è necessario un Cast per convertire l’oggetto in modo tale da assegnarlo ad un reference del tipo “Persona.”

[csharp]
FileStream fsr = File.OpenRead("serializzato.xml");
Persona per = (Persona)dcs.ReadObject(fsr);
[/csharp]

Nel caso dobbiamo creare dei file JSON invece dei file XML, dobbiamo semplicemente sostituire la classe  DataContractSerializer con la classe DataContractJsonSerializer.

NetDataContractSerializer

L’ultimo esempio di serializzatore con contratto che vedremo è il : NetDataContractSerializer.

Questo serializzatore è molto simile al DataContractSerializer, infatti serializza gli XML con una limitazione: vi p bisogno che il tipo serializzato è presente nella CLR che deserializza il file.

La serializzazione è simile, eccetto per il fatto che non vi è bisogno di passare il tipo da serializzare nel costruttore, tale tipo infatti dev’essere presente nel software. Il serializzatore è in grado di serializzare gli oggetti con
DataContractAttribute  oppure con i SerializableAttribute e serializza anche i tipi che implementano ISerializable (infatti è possibile usare Serialize e Deserialize).

[csharp]
NetDataContractSerializer dcs = new NetDataContractSerializer();
….
FileStream fs = File.OpenWrite("serializzato.xml");
dcs.WriteObject(fs,p);

[/csharp]

Ometto per brevità la deserializzazione che è la medesima.

Serializzazione senza contratto

Precedente alla serializzazione con DataContract esiste anche la serializzazione basata sulla struttura degli oggetti. Tale tipo di serializzazione è decisamente più macchinosa, ma permette di avere maggior controllo sulla serializzazione.

Normalmente si possono serializzare gli oggetti in due formati, in Binario e in XML rispettivamente con il BinaryFormatter e il XmlSerializer.

BinaryFormatter

Tale tecnica consiste nel salvare le classi utilizzando il formato binario, in questo caso il serializzatore salverà integralmente l’oggetto affinché possa essere recuperato, contrariamente ai metodi con DataContract il metodo da utilizzare sono Serialize Deserialize.

Innanzitutto aggiorniamo la classe Persona affinché sia pulita per la serializzazione senza contratto.

[csharp]
[Serializable]
public class Persona
{
public string nome {
get { return _nome; }
set { _nome = value; }
}
private string _nome;

public string cognome {
get { return _cognome; }
set { _cognome = value; }
}
private string _cognome;

public string Indirizzo
{
get {return indirizzo;}
set {indirizzo = value;}
}
private string fattiSpece;

public void setMiaFattispece(String str) {
fattiSpece = str;
}
private string indirizzo;

}

static void SerializzazioneBinaria(){
Persona p = new Persona();
p.cognome = "Ronaldo";
p.nome = "Cristiano";
p.Indirizzo = "Napoli, via dei martiri 1";
p.setMiaFattispece("assente");

//creo lo stream di scrittura
FileStream fs = File.OpenWrite("serializzato.bin");
BinaryFormatter b = new BinaryFormatter();
b.Serialize(fs,p);
fs.Flush();
fs.Close();
}

FileStream fsr = File.OpenRead("serializzato.bin");
Persona np = (Persona)b.Deserialize(fsr);
fsr.Close();
[/csharp]

In questo caso otterremo che l’oggetto verrà serializzato nel suo complesso, infine notiamo che l’uso dell’attributo Serializable è obbligatorio per effettuare la serializzazione binaria.

Infine se non vogliamo serializzare alcuni attributi pubblici è possibile utilizzare l’attributo NonSerialized.

XmlSerializer

Operativamente il serializzatore XmlSerializer è molto simile al BinaryFormatter.

[csharp]
XmlSerializer dcs = new XmlSerializer(typeof(Persona));
Persona p = new Persona();
p.cognome = "Milik";
p.nome = "Arek";
p.Indirizzo = "via dei martiri 13";
p.setMiaFattispece("assente");
Console.WriteLine(p.ToString());

FileStream fs = File.OpenWrite("serializzato.xml");
dcs.Serialize(fs,p);
fs.Flush();
fs.Close();

FileStream fsr = File.OpenRead("serializzato.xml");
Persona per = (Persona)dcs.Deserialize(fsr);

Console.WriteLine("Deserializzato da file");
Console.WriteLine(per.ToString());
Console.ReadKey();

fsr.Close();
[/csharp]

Facendo girare l’esempio osserveremo una grande prima differenza, il field fattiSpecie non verrà serializzato, perché sono serializzati soltanto gli attributi pubblici.

Inoltre XmlSerializer permette di definire degli attributi per regolare in maniera fine la serializzazione:

  1. XmlRootAttribute, permette di definire gli attributi del nodo root dell’oggetto, in particolare si possono utilizzare le seguenti opzioni:
    1. ElementName, permette di definire il nome del nodo da visualizzare come root.
    2. Namespace, permette di definire il namespace da utilizzare.
    3. IsNullable, permette di impostare, tramite true o false, se l’elemento può essere nullo.
    4. DataType, preleva o setta, tramite uno schema XSD da utilizzare nella serializzazione.
  2. XmlElement, permette di impostare il campo come un elemento dell’albero XML.
    1. ElementName, permette di definire il nome del nodo da visualizzare nel file XML.
    2. DataType, permette di impostare, tramite una stringa, il tipo dell’elemento.
    3. Form, permette di attribuire un valore che permette di esprimere se un elemento è qualificato.
    4. IsNullable, permette di impostare, tramite true o false, se l’elemento può essere nullo,
    5. Namespace, permette di definire il namespace da utilizzare.
    6. Order, permette di definire l’ordine di apparizione del campo all’interno del file serializzato.
    7. Type, permette di impostare, tramite una stringa, il tipo dell’elemento.
  3. XmlAttribute, permette di impostare il campo come un attributo del nodo radice. Se si vuole impostare l’attributo ad un nodo intermedio, il nodo itermedio dev’essere convertito a classe ed effettuare materialmente una inclusione.
    1. ElementName, permette di definire il nome dell’attributo del nodo.
    2. DataType, preleva o setta, tramite uno schema XSD da utilizzare nella serializzazione.,
    3. Form, permette di attribuire un valore che permette di esprimere se un elemento è qualificato.
    4. Namespace, permette di definire il namespace da utilizzare.
    5. Type, permette di impostare, tramite una stringa, il tipo dell’elemento.
  4. XmlIgnore, permette di ignorare il campo dell’oggetto anche se questo è public.
Differenze tra DataContractSerializer e XmlSerializer

A questo punto è lecito porsi una domanda: quando conviene utilizzare un contratto?

La risposta (come sempre nel caso dell’informatica) è dipende.

Sostanzialmente nel caso degli XmlSerializer c’è la possibilità di dichiarare gli attributi utilizzando il tag “XmlAttribute” cosa che non è possibile effettuare nel DataContract, tuttavia il DataContract permette di passare da JSON a XML con molta semplicità.

Un caso particolare, il JavaScriptSerializer

Il JavaScriptSerializer è un serializzatore senza contratto che permette di effettuare le operazioni di serializzazione di JSON su stringa. L’utilizzo di tale serializzatore è scoraggiato da Microsoft ed è utilizzato solo per fini interni al framework .Net.

Vediamone l’utilizzo:

[csharp]
JavaScriptSerializer dcs = new JavaScriptSerializer();
StringBuilder fs = new StringBuilder();

Persona p = new Persona();
p.cognome = "Pavoletti";
p.nome = "Leonardo";
p.Indirizzo = "via dei martiri 13";
p.setMiaFattispece("assente");
Console.WriteLine(p.ToString());

dcs.Serialize(p, fs);
string fsr = fs.ToString();
Persona per = (Persona)dcs.Deserialize(fsr, typeof(Persona));
[/csharp]

La serializzazione avviene seguendo le annotazioni utilizzate per la classe XMLSerializer.

Pertanto quest’ultimo modo di serializzare che vediamo, rende la serializzazione senza contratto ugualmente potente rispetto alla serializzazione con contratto.

Facilitazioni alla serializzazione senza contratto

Opzionalmente è possibile implementare l’interfaccia ISerializable e utilizzare i SerializableAttribute.

ISerializzable

L’interfaccia ISerializzable consente ad una classe di disporre del metodo GetObjectData che permette di eseguire delle operazioni prima della serializzazione al fine di arricchire ulteriormente le informazioni della serializzazione.

SerializableAttribute

L’attributo SerializableAttribute permette di indicare che una classe è Serializzabile, si può abbreviare utilizzando semplicemente Serializzable (che abbiamo visto per la serializzazione binaria).

Inoltre è possibile utilizzare degli attributi che permettono di definire dei metodi da eseguire durante la serializzazione, questi metodi sono:

  • OnSerializing, il metodo verrà chiamato prima di effettuare la serializzazione.
  • OnSerialized, il metodo chiamato appena terminata la serializzazione.
  • OnDeserializing, il metodo chiamato prima di effettuare la deserializzazione.
  • OnDeserialized, il metodo chiamato appena terminata la deserializzazione.

[csharp]
[OnSerializing()]
internal void OnSerializingMethod(StreamingContext context)
{
//Utile per impostare dei valori ad-hoc della serializzazione
cognomeLegale = "Cognome";
}

[OnSerialized()]
internal void OnSerializedMethod(StreamingContext context)
{
//utile per resettare i valori dopo la serializzazione
cognomeLegale = "";
}

[OnDeserializing()]
internal void OnDeserializingMethod(StreamingContext context)
{
//utile per impostare dei valori ad-hoc durante la deserializzazione
cognomeLegale = "Cognome";
}

[OnDeserialized()]
internal void OnDeserializedMethod(StreamingContext context)
{
//utile per resettare i valori a valle della deserializzazione
cognomeLegale = "";
}
[/csharp]

Conclusioni

Abbiamo visto che la serializzazione è quel processo per cui si vuole che delle istanze di un determinato Tipo possano essere trasformate in oggetti manipolabili dalla CRL mediante strutture generiche come stringhe e file. Abbiamo visto che vari serializzatori che ci permettono di definire il tipo di serializzazione che vogliamo usare nel nostro software: i tipi più famosi di serializzazione sono Binario, XML e JSON ottenibili con: BinaryFormatter, DataContractSerializer, XmlSerializer e DataContractJsonSerializer.

Abbiamo visto i due “modi di pensare” della serializzazione, ovvero tramite il DataContract oppure senza, infine abbiamo visto le varie “eccezioni” ovvero: JavaScriptSerializer, NetDataContractSerializer.

Ho tentato di essere quanto più sintetico ed essenziale nell’articolo, ma la mole di informazioni è notevole. Se riscontrate degli errori, vi invito ad utilizzare i commenti.

Gestire la fine del ciclo di vita degli oggetti

In questo articolo voglio illustrare la gestione della fine del ciclo di vita degli oggetti in C-Sharp.

Il nostro software gira in una porzione del sistema operativo chiamata Common Language Runtime Environment (CLR). Il ruolo di tale compente nel sistema è tener traccia di tutte le risorse proprie del software e di permettere l’accesso alle risorse esterne ad esso. Il software in C-Sharp è composto da una pletora di oggetti che (si spera) abbiano responsabilità e ruoli ben definiti.

Come avviene in Java, anche in C-Sharp un oggetto termina la sua vita all’interno del nostro software per mano del Garbage Collector (GC). Questo serial killer è la mano armata del CLR, viene chiamato in causa quando c’è scarsità di memoria e pertanto è necessaria una pulizia per fare spazio. Il GC determina quali sono gli oggetti da eliminare mediante un algoritmo di raggiungibilità di tipo “mark and compact“, ma quello che ci serve sapere è che esamina la memoria heap nel sistema al fine di trovare oggetti non più raggiungibili, se ne trova uno, lo distrugge.

Un modo gentile di procedere alla eliminazione degli oggetti è di porne i riferimenti a null, questa è la prassi consigliata. Porre a null il riferimento permette di rendere irraggiungibile l’oggetto rendendolo eligibile per il GC.

Non tutti gli oggetti hanno la stessa natura e pertanto non tutti gli oggetti si possono “smaltire” in questo modo così semplice, gli oggetti rappresentano le tipologie di risorse più varie e le risorse si suddividono in due grandi gruppi:

  • Risorse Gestite, ovvero le risorse che utilizzano gli oggetti direttamente controllabili dal framework e quindi di cui si può conoscere immediatamente lo stato, ad esempio un ArrayList è una risorsa gestita.
  • Risorse non gestite, ovvero le risorse che utilizzano oggetti non direttamente controllabili dal framework, esterne al software, l’esempio tipico di questa categoria sono i File. 

Il framework .NET tiene a cuore che il buon sviluppatore tenda ad ottimizzare l’uso della memoria, o almeno a farne un utilizzo criteriato delle risorse disposizione, pertanto ha creato un modello di utilizzo delle risorse chiamato disposable pattern e un’interfaccia ad-hoc, la IDisposable, per agevolare gli sviluppatori alla gestione corretta della memoria.

L’interfaccia IDisposable mette a disposizione il solo metodo Dispose.

L’utilizzo dell’interfaccia IDisposable è fortemente consigliato quando si utilizzano risorse non gestite, ma in generale si può implementare anche per liberare risorse gestite dalla CLR, inoltre l’implementazione dell’interfaccia IDisposable permette di utilizzare il costrutto Using che rende naturale il concetto di instanziazione e dismissione dell’oggetto utilizzato: infatti l’ultima istruzione chiamata al termine del costrutto Using è il metodo dispose.

Da qui capiamo che implementando IDisposable il CLR non chiama “da sé” il metodo Dispose per dismettere l’oggetto, bensì chiama un altro metodo, il Finalizzatore.

Il finalizzatore è chiaramente collegato al discorso dell’interfaccia IDisposable, ma interrompiamo un momento il discorso sull’interfaccia per comprenderne meglio il ruolo di questa funzione nel nostro software.

Il finalizzatore è una funzione particolare, ha il compito di distruggere l’oggetto e non può essere chiamato direttamente dal codice ma solo dal GC. Infine il finalizzatore non può essere dichiarato public static. 

Il codice del finalizzatore da un primo sguardo sembra un costruttore, infatti ha una sintassi simile se non per il fatto che prima del nome della classe è presente una tilde.

[csharp]
~Persona()
{
//qui chiamiamo il codice per finalizzare l’oggetto
}[/csharp]

Nel Finalizzatore bisogna inserire il codice che elimina solo le risorse non gestite.
Il motivo è immediato: se viene chiamato il finalizzatore dell’oggetto vuol dire che dev’essere dismesso e che tutte le risorse collegate dall’oggetto sono già state distrutte.

Da quello che abbiamo appena riportato, va da sé che un possibile codice che effettua la distruzione dell’oggetto potrebbe essere:

[csharp]
public void Dispose(){
dispose(true);
System.GC.SuppressFinalize(this);
}

public void Dispose(bool managedResources){
if(managedResources){
//liberiamo le risorse gestite
liberaRisorseGestite();
}
//liberiamo le risorse non gestite
liberaRisorseNonGestite();
}
}

~Persona(){
Dispose(false);
}
[/csharp]

Il metodo System.GC.SuppressFinalize(this) permette di rimuovere l’oggetto dalla lista di Finalizzazione degli oggetti, poiché il passaggio del GC sarebbe inutile, visto che è stato già distrutto.
Nel caso dell’utilizzo del nostro oggetto nel costrutto Using avviene che al termine dell’utilizzo l’oggetto verrà distrutto. Mentre se sarà il GC a distruggere il nostro oggetto, allora verranno dismesse le sole risorse non gestite, poiché solo quelle risorse non possono essere cancellate in autonomia dal CLR.

Pertanto come dobbiamo comportarci nell’implementare l’interfaccia?

  • Se abbiamo solo delle risorse non gestite dobbiamo implementare l’interfaccia IDisposable e il finalizzatore.
  • Se abbiamo solo delle risorse gestite dobbiamo implementare solo l’interfaccia IDisposable.
  • Se abbiamo delle risorse gestite e non gestite dobbiamo implementare l’interfaccia IDisposable e il finalizzatore.

Ultima nota, tutt’altro che marginale è che chiamare due volte il dispose, non deve causare il sollevamento di eccezioni!!!!

Gerarchia di Costruttori in C#

In questa breve panoramica, voglio illustrare il concetto di Gerarchia di costruttori così come interpretata da C# confrontandola con Java.

Consideriamo ad esempio le classi Persona e Manager: Persona è superclasse di Manager.

In Java il costruttore per Manager è strutturato di default in questo modo:

[java]
public Manager(){
super();
}
[/java]

Dove super sarà il costruttore di default di Persona, questo comporta che se la classe Persona non ha il costruttore di default allora avremo un errore in compilazione.

Pertanto assumiamo che la classe Persona definisca il costruttore che segue:

[java]
public Persona(String nome, String cognome) {
this.nome=nome;
this.cognome=cognome;
}[/java]

Dobbiamo modificare il costruttore di senza parametri Manager:

[java]
public Manager(){
super("cognome","nome");
}[/java]

Volendo essere più puliti, aggiungiamo un costruttore parametrico.

[java] public Manager(String cognome, String nome){
super(cognome,nome);
}[/java]

Ricordiamo che la prima istruzione del nuovo costruttore dev’essere la chiamata al costruttore della superclasse.
A questo punto ci poniamo una domanda: Come chiamiamo un’altro costruttore della stessa classe?

Ovvero come possiamo chiamare Manager(String cognome, String nome) da Manager()?

Utilizzeremo la keyword this, che in questo caso assume un significato diverso dal normale riferimento implicito all’oggetto.

[java] public Manager(String cognome, String nome){
this();
this.cognome=cognome;
this.nome=nome;
}[/java]

In java abbiamo più comportamenti ambigui:

La chiamata al costruttore deve essere la prima istruzione del costruttore, non sono ammesse più chiamate a più costruttori. Non aspettatevi chiamate del tipo:

[java] public Manager(String cognome, String nome){
this();
this(nome,cognome,indirizzo) //errore di compilazione.
this.cognome=cognome;
this.nome=nome;
}[/java]

Alla riga 3 avremo errore di compilazione.

In C# il concetto resta il medesimo, cambia in modo rilevante la sintassi che impedisce di fatto di avere errori di compilazione come visto nell’ultimo esempio.

La tecnica è questa: Il costruttore da richiamare non viene inserito come prima istruzione del costruttore stesso, ma come suffisso all’istruzione che dichiara il costruttore.

[csharp]public Persona():this("Nome","Cognome")
{
System.Console.WriteLine("Persona");
}

public Persona(String nome, String cognome)
{
System.Console.WriteLine("Persona più parametri");
}
[/csharp]

L’output in questo caso sarà:

Persona più parametri

Persona

Identico al comportamento di Java.

Se si vuole chiamare un particolare costruttore della superclasse bisognerà utilizzare la keyword “base” (in java abbiamo visto che è “super“).

[csharp]
public Manager():base("nome","cognome")
{
System.Console.WriteLine("Manager");
}

[/csharp]

La differenza tra i due linguaggi può sembrar “effimera”, tuttavia bisogna osservare che in effetti in Java la chiamata ad un altro costruttore non passa facilmente in secondo piano, avendo a tutti gli effetti dignità di istruzione.
Mentre in C# viene vista, in un certo senso, come una “aggiunta” al costruttore, appesantendone la sintassi e rendendo macchinosa e non immediata la comprensione, specialmente nel caso in cui ci siano molti parametri nel costruttore.

ad esempio, in java:

[java] public Manager(String nome){
this(nome,"Cognome");
}

public Manager(String nome, String Cognome){
this.nome= nome;
this.cognome = cognome;
}
[/java]

Mentre in C# avremo:

[csharp]
public Manager(String nome):base(nome,"cognome")
{ }
public Manager(String nome, String cognome){
this.nome= nome;
this.cognome = cognome;
}
[/csharp]

Per concludere, in C# abbiamo una sintassi orientata ad evitare errori in compilazione,
in Java si vuole che sia chiara e immediata la comprensione del fatto che si utilizza un gerarchia di costruttori.