Questo sito utilizza cookies, anche di terze parti, per mostrare pubblicità e servizi in linea con il tuo account. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - Serializzazione di oggetti

Guida al Visual Basic .NET

Capitolo 100° - Serializzazione di oggetti

<< Precedente Prossimo >>

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:

  • Binary : i dati vengono salvati in formato binario, conservando solamente i bit effettivi di informazione. A causa della sua natura, i valori processati con la serializzazione binaria sono più compatti e l'operazione è assai veloce, tuttavia essi non sono leggibili né dall'utente né dal programmatore. Questo non è un grave difetto, poiché praticamente sempre non si deve intervenire sui supporti di memorizzazione: basta che funzionino correttamente
  • SOAP : i dati vengono salvati in un formato intellegibile, ossia interpretabili dall'uomo. In questo caso, tale formato si identifica con l'XML, per mezzo del quale le informazioni vengono persistite seguendo le direttive del Simple Object Access Protocol. Questo tipo di serializzazione richiede più memoria e un tempo di elborazione maggiore, ma può essere compresa dall'uomo. Ad esempio può risultare utile nell'inviare dati ad applicazioni parser che li visualizzano in schemi ordinati. Ad ogni modo, la serializzazione SOAP è marchiata come obsoleta anche nel Framework 2.0, a favore della più rapida e meno dispendiosa Binary
  • XML : simile a quella SOAP, tranne per il fatto che viene utilizzato l'oggetto XmlSerializer come formatter e che gli attributi che influenzano la serializzazione normale non vengono interpretati con l'uso di questa tecnica. Ci sono molti altri piccoli particolari che li differenziano, ma li si vedrà nei prossimi esempi

 

Serializzare oggetti

Le 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.
Non posso mostrare un esempio della serializzazione Soap, purtroppo, perchè nel momento in cui scrivo ho ancora il framework 2.0, nel quale il namespace relativo non esiste. Posso invece mostrare tale esempio con l'XmlFormatter su una lista di oggetti:

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.
Un altro inconveniente legato alla serializzazione è costituito dagli eventi. Come spiegato tempo fa, essi non sono altro che delegate, i quali a loro volta sono pur sempre tipi derivati da System.Delegate e, in quanto tipi, sono serializzabili. Nulla di male fin qui, ma quella che ho prima definito come "astuzia" del CLR nel riprodurre grafi corretti, si trasforma ora in un incredibile spauracchio. Mi spiego meglio. Durante il processo di salvataggio, il formatter cattura tutti gli oggetti raggiungibili direttamente o indirettamente attraverso le proprietà e i campi di una classe. Allo stesso modo, in un delegate sono raggiungibili anche tutti gli oggetti sottoscrittori, ossia quelli che hanno registrato alcuni dei propri metodi come gestori d'evento dell'oggetto radice. Questo produce due gravi effetti collaterali: vegono serializzati anche tutti gli altri oggetti coinvolti e, con loro, praticamente tutta l'applicazione, il che, oltre a essere un enorme dispendio di spazio, non è il risultato desiderato; se anche uno solo di tutti gli oggetti nel grafo non è serializzabile, il processo fallisce lanciando un'eccezione. Partendo dal presupposto che i Form non sono serializzabili, nel 99% dei casi, si otterrebbe lo stesso errore. L'unico modo per ridurre i danni è comunicare al formatter di non serializzare gli eventi, attraverso l'attributo NonSerialized (che vedremo fra breve): questo implica definire l'evento come custom. Poiché sono utilizzati raramente, non ho trattato gli eventi custom, ma ecco un esempio:

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.
Per modificare il comportamente del formatter, è possibile usare alcuni attributi. Eccone una breve lista:

  • NonSerialized : il campo viene saltato durante il processo di salvataggio. Oltre a poter evitare fastidiose ripercussioni sul codice come quella analizzata poco fa, contribuisce a risparmiare un pochetto di memoria in più. Solitamente questo attributo viene applicato a quei valori che possono essere dedotti da altri campi (ad esempio, l'età, che può essere calcolata partendo dalla data di nascita) o che al prossimo caricamente sicuramente non conterranno più valori validi (come handle di finestre o di altre risorse non gestite). Solo i formatter Binary e Soap tengono conto di NonSerialized, al contrario di Xml
  • OptionalField : il campo viene serializzato normalmente, ma nel processo di caricamento dei dati la sua mancanza non produce alcun errore. Nel caso il campo non sia presente, assume il valore di default della sua categoria: 0 per i numeri, Nothing per i tipi reference

 

Serializzazione custom

I 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".
Altri attributi che si possono usare sono OnSerialization, OnSerialized, OnDeserialization e OnDeserialized, che, se applicati a un metodo, lo eseguono nelle varie fasi della serializzazione: prima o dopo il salavatggio; prima o dopo il caricamento. Per mezzo di questi è anche possibile impostare campi opzionali che devono assumere un determinato valore per essere validi.

<< Precedente Prossimo >>
A proposito dell'autore

Programmatore e analista .NET 2005/2008/2010 (in particolare C# e VB.NET), anche nell'implementazione Mono per Linux. Conoscenze approfondite di Pascal, PHP, XML, HTML 4.01/5, CSS 2.1/3, Javascript (e jQuery). Conoscenze buone di C, LUA, GML, Ruby, XNA, AJAX e Assembly 68000. Competenze basilari di C++, SQL, Hlsl, Java.