Guida al Visual Basic .NET
Capitolo 100° - Serializzazione di oggetti
La serializzazione consiste nel salvare un oggetto su un qualsiasi supporto compatibile (file, flussi di memoria, variabili, stream, eccetera...) per poi poterlo ricaricare in ogni momento: questo processo crea di fatto una copia perfetta dell'oggetto di partenza. Il framework .NET è in grado di serializzare tutti i tipi base, compresi anche gli array di tali tipi: poiché tutte le strutture e le classi utilizzano i tipi base, praticamente ogni oggetto può essere sottoposto senza problemi a un processo di serializzazione di default, anche se in certi casi sorgono problemi che, giustamente, spetta al programmatore risolvere. Esistono tre possibili tipi di serializzazione, ognuno associato a un determinato formatter, ossia un oggetto capace di trasferire i dati sul supporto:
Serializzare oggettiLe classi necessarie alla serializzazione si trovano nei namespace System.Runtime.Serialization e System.Xml.Serialization. Le classi da usare sono BinaryFormatter e SoapFormatter, definite nei rispettivi namespace Binary e Soap. Ecco un esempio della prima: Imports System.Runtime.Serialization.Formatters Module Module1 _ Class Person Implements IComparable Protected _FirstName, _LastName As String Private ReadOnly _BirthDay As Date Public Property FirstName() As String Get Return _FirstName End Get Set(ByVal Value As String) If Value <> "" Then _FirstName = Value End If End Set End Property Public Overridable Property LastName() As String Get Return _LastName End Get Set(ByVal Value As String) If Value <> "" Then _LastName = Value End If End Set End Property Public ReadOnly Property BirthDay() As Date Get Return _BirthDay End Get End Property Public Overridable ReadOnly Property CompleteName() As String Get Return _FirstName & " " & _LastName End Get End Property Public Overloads Overrides Function ToString() As String Return MyBase.ToString End Function Public Overloads Function ToString(ByVal FormatString As String) _ As String Dim Temp As String = FormatString Temp = Temp.Replace("{F}", _FirstName) Temp = Temp.Replace("{L}", _LastName) Return Temp End Function Public Function CompareTo(ByVal obj As Object) As Integer _ Implements IComparable.CompareTo 'Un oggetto non-nothing (questo) è sempre 'maggiore di un oggetto Nothing (ossia obj) If obj Is Nothing Then Return 1 End If Dim P As Person = DirectCast(obj, Person) Return String.Compare(Me.CompleteName, P.CompleteName) End Function Sub New(ByVal FirstName As String, ByVal LastName As String, _ ByVal BirthDay As Date) Me.FirstName = FirstName Me.LastName = LastName Me._BirthDay = BirthDay End Sub End Class Sub Main() 'Crea un nuovo oggetto Person da serializzare Dim P As New Person("Pinco", "Pallino", New Date(1990, 6, 1)) 'Crea un nuovo Formatter binario Dim Formatter As New Binary.BinaryFormatter() 'Crea un nuovo file su cui salvare l'oggetto Dim File As New IO.FileStream("C:person.dat", IO.FileMode.Create) 'Serializza l'oggetto 'Attenzione! I Formatter definiti in 'System.Runtime.Serilization, 'a differenza di quello in System.Xml.Serialization, 'richiedono esplicitamente che un oggetto sia 'dichiarato serializzabile, anche se questo lo è 'logicamente. Per questo, bisogna recuperare la vecchia 'classe Person e applicarvi l'attributo Serializable. 'Per rinfrescarvi la memoria, ve ne ho scritto 'una copia sopra Formatter.Serialize(File, P) 'E chiude il file File.Close() Console.WriteLine("Salvataggio completato!") 'Crea una nuova variabile di tipo Person per contenere 'i dati caricati dal file Dim F As Person 'Apre lo stesso file di prima, ma in lettura Dim Data As New IO.FileStream("C:person.dat", IO.FileMode.Open) 'Carica le informazioi salvate nel file F = Formatter.Deserialize(Data) 'Verifica che F e P siano perfettamente uguali Console.Write("F = P -> ") Console.Write(F.CompareTo(P)) '> Ricordate che CompareTo restituisce 0 nel caso di uguaglianza Console.ReadKey() End Sub End Module
Se si prova ad aprire il file person.dat, si trovano molti caratteri incomprensibili intervallati da altri nomi leggibili, tra i quali si
possono leggere il nome completo dell'assembly e i nomi dei campi serializzati. Infatti, quando si serializza in binario, vengono salvati
anche tutti i riferimenti agli assembly a cui il tipo dell'oggetto salvato appartiene, insieme coi nomi dei campi e i loro valori binari. Public Module Module1 '... Sub Main() 'Crea nuovi oggetti Person da serializzare Dim P1 As New Person("Pinco", "Pallino", New Date(1990, 6, 1)) Dim P2 As New Person("Tizio", "Caio", New Date(1967, 4, 13)) Dim P3 As New Person("Mario", "Rossi", New Date(1954, 8, 12)) 'Ho creato un array perchè è più veloce, ma nulla 'vieta di usare liste generics o qualsiasi altro tipo 'di collezione Dim Persons() As Person = {P1, P2, P3} 'Crea un nuovo Formatter Xml. Il serializzatore in questo 'caso ha bisogno anche dell'oggetto Type relativo 'all'oggetto da serializzare (un array di person). Dim Formatter As New Serialization.XmlSerializer(GetType(Person())) 'Crea un nuovo file su cui salvare l'oggetto Dim File As New IO.FileStream("C:persons.dat", IO.FileMode.Create) 'Serializza l'oggetto 'Attenzione! Se gli XmlSerializer non hanno bisogno che 'l'oggetto in questione possegga l'attributo Serializable, 'hanno invece bisogno che questo esponga almeno un 'costruttore senza parametri. Quindi bisogna aggiungere 'un nuovo New() a Person. Inoltre, altra limitazione 'importante, con questo formatter è possibile 'serializzare solo tipi pubblici. Formatter.Serialize(File, Persons) 'E chiude il file File.Close() Console.WriteLine("Salvataggio completato!") 'Potrete constatare che il salvataggio impiega un 'tempo notevolmente maggiore 'Crea una nuova variabile di tipo Person per contenere 'i dati caricati dal file Dim F As Person() 'Apre lo stesso file di prima, ma in lettura Dim Data As New IO.FileStream("C:persons.dat", IO.FileMode.Open) 'Carica le informazioi salvate nel file F = Formatter.Deserialize(Data) 'Verifica che F e P siano perfettamente uguali For I As Byte = 0 To 2 Console.WriteLine("P{0} = F{0} -> {1}", I, _ Persons(I).CompareTo(F(I))) Next '> Ricordate che CompareTo restituisce 0 nel caso di uguaglianza Console.ReadKey() End Sub End Module Se si prova ad aprire il file persons.dat, si trova un normalissimo file xml: <?xml version="1.0"?> PincoPallinoTizioCaioMarioRossi
Problemi legati alla serializzazione
Potrebbe capitare di avere dei riferimenti circolari all'interno dei campi di un oggetto, ad esempio un principale e un dipendente che
si puntano vicendevolmente. In questi casi la serializzazione Xml fallisce miseramente e questo costituisce un'altra delle gravi pecche che
essa porta con sé: se si tenta l'operazione, viene lanciata un'eccezione con il messaggio "Individuato riferimento circolare", e tutto
il meccanismo cade rovinosamente. Al contrario, il binary formatter riesce a individuare casi del genere e si limita a serializzare l'oggetto
che causa il riferimento circolare una sola volta, arginando tutti i possibili problemi. Quando ci si trova in situazioni di questo tipo, o
anche quando si hanno dei riferimenti ricorsivi, la struttura che si forma a partire da un oggetto si dice grafo: si può dire
che tutti i riferimenti "germoglino" dall'unico oggetto, detto appunto "radice". Proprio per la capacità di individuare e risolvere
problematiche di questo tipo, il binary formatter costituisce la soluzione migliore alla clonazione Deep di oggetti. Si era infatti parlato,
nel capitolo sull'interfaccia ICloneable, di come il metodo MemberwiseClone si limiti solo a una copia superficiale, clonando esclusivamente
i campi non reference dell'istanza (clonazione Shallow). La clonazione deep, invece, ricostruisce tutto il grafo dell'istanza. Module Module1 Public Class EventLauncher Private _ID As Int32 'Negli eventi custom, bisogna usare una variabile privata 'dello stesso tipo dell'evento da lanciare. Si può 'vedere un elemento custom un pò come una proprietà, 'che media l'interazione con il vero delegate gestore Private _IDChangedEventHandler As EventHandler 'Dichiara il nuovo evento Public Custom Event IDChanged As EventHandler 'Proprio come nelle proprietà ci sono i 'blocchi Get e Set per ottenere e assegnare un valore, 'qua di sono i blocchi AddHandler e RomveHandler 'per aggiungere o rimuovere un gestore d'evento e 'il blocco RaiseEvent per lanciarlo AddHandler(ByVal Value As EventHandler) 'Basta richiamare System.Delegate.Combine per 'aggiungere il nuovo gestore Value _IDChangedEventHandler = _ [Delegate].Combine(_IDChangedEventHandler, Value) End AddHandler RemoveHandler(ByVal Value As EventHandler) _IDChangedEventHandler = _ [Delegate].Remove(_IDChangedEventHandler, Value) End RemoveHandler RaiseEvent(ByVal sender As Object, ByVal e As EventArgs) 'Controlla che ci sia almeno un gestore, quindi li 'richiama tutti If _IDChangedEventHandler IsNot Nothing Then _IDChangedEventHandler(sender, e) End If End RaiseEvent End Event Public Property ID() As Int32 Get Return _ID End Get Set(ByVal Value As Int32) _ID = Value RaiseEvent IDChanged(Me, EventArgs.Empty) End Set End Property End Class '... End Module
Questo codice evidenzia come sia difficile gestire gli eventi nella serializzazione.
Serializzazione customI tipi forniti dal Framework .Net espongono metodi capaci di risolvere praticamente ogni casistica di problemi e perciò solo in rari casi si ricorre alla serializzazione custom. Questo tipo di serializzazione non interviene nei meccanismi che modificano fisicamente il supporto di memorizzazione e neanche in quelli che recuperano i dati da questo, ma agisce prima e dopo che tali azioni vengano compiute. Per creare un oggetto con queste caratteristiche, si deve implementare l'interfaccia ISerializable, la quale espone solo un metodo: GetDataObject. Esso ha il compito di selezionare, tra tutti i campi disponibili, quali persistere e quali no, anche sulla base di certe condizioni, e viene invocato prima che abbia inizio il processo di serializzazione. Inoltre, l'oggetto deve anche esporre un costruttore Private o Protected (a seconda che si debba ereditare oppure no) con una particolare signature. Ecco un esempio: Module Module2 _ Public Class Client Implements ISerializable Private _Name As String _ Private _IP As String Private _IsIPStatic As Boolean Public Property Name() As String Get Return _Name End Get Set(ByVal Value As String) _Name = Value End Set End Property 'ReadOnly perchè l'IP viene deciso alla connessione 'e poi non subisce cambiamenti Public ReadOnly Property IP() As String Get Return _IP End Get End Property 'L'IP rilevato dal server, normalmente, cambia ad ogni 'connessione e quindi sarebbe inutile serializzarlo. 'Tuttavia, se viene reso statico, ad esempio con 'l'uso di un DNS, allora lo si dovrebbe serializzare 'e ricaricare al successivo avvio. Questa proprietà 'ne definisce lo stato e influenza i processi di 'serializzazione e deserializzazione Public Property IsIPStatic() As Boolean Get Return _IsIPStatic End Get Set(ByVal Value As Boolean) _IsIPStatic = Value End Set End Property 'GetDataObject seleziona i campi da serializzare Private Sub GetDataObject(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) _ Implements ISerializable.GetObjectData 'Info si comporta come un dictionary(of String, Object). 'Basta aggiungere i valori da salvare info.AddValue("Name", Me.Name) info.AddValue("IsIPStatic", Me.IsIPStatic) 'Questo passaggio è attuabile solo con la 'serializzazione custom If Me.IsIPStatic Then info.AddValue("IP", Me.IP) End If End Sub 'Private New viene richiamato dal formatter dopo la 'deserializzaziore per impostare i valori Private Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) Me.Name = info.GetString("Name") Me.IsIPStatic = info.GetBoolean("IsIPStatic") If Me.IsIPStatic Then _IP = info.GetString("IP") Else _IP = "127.0.0.1" End If End Sub 'Un costruttore pubblico deve comunque esserci Sub New(ByVal Name As String, ByVal IP As String) Me.Name = Name _IP = IP End Sub End Class Sub Main() 'Crea un nuovo client Dim C As New Client("Totem", "86.45.8.23") Dim Formatter As New Binary.BinaryFormatter() Dim File As New IO.FileStream("C:client.dat", IO.FileMode.Create) 'Lo serializza, con IP dinamico C.IsIPStatic = False Formatter.Serialize(File, C) File.Close() 'Lo ricarica, e osserva che l'IP è stato impostato 'su quello della macchina locale Dim Data As New IO.FileStream("C:client.dat", IO.FileMode.Open) C = Formatter.Deserialize(Data) Console.WriteLine(C.IP) ' > 127.0.0.1 Data.Close() Console.ReadKey() End Sub End Module
Impostando IsIPStatic a True, l'output cambierà in "86.45.8.23".
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|