Introdução
"Serialization" consiste em guardar um objecto num elemento de armazenamento (um ficheiro, uma base de dados, um "buffer de memória, etc.), permitindo a sua posterior reutilização, através de um processo de "deserialization", voltando a recriar o objecto na sua forma original.
A Serialização é um processo chave da plataforma .Net sendo utilizado de forma transparente para funcionalidades tais como por exemplo para enviar um objecto por valor para outra aplicação.
Um objecto deve ser tornado serializável se se pretender envia-lo para outra aplicação ou grava-lo em disco ou base de dados (Por exemplo para guardar um objecto num objecto de sessão, na camada de apresentação de uma aplicação "web").
O conceito de persistência é complementar ao conceito de serialização. Ambos consistem no armazenamento de um objecto, mas o conceito de persistência define que o meio utilizado é persistente (por exemplo um ficheiro) enquanto o conceito de serialização é independente da persistência do meio (por exemplo, na memória RAM).
Conceitos básicos
A "framework" .Net sabe como serializar todos os tipos básicos de dados, incluindo "integers", "strings" e "arrays" numéricos e de "strings", por isso é possível manter estes objectos serializados em objectos de "stream" (ficheiros por exemplo) com um esforço mínimo. O único requisito para esta operação é a criação de um objecto "formatter" adequado.
Um objecto "formatter" é um objecto que implementa o interface "IFormater" definido no namespace "System.Runtime.Serialization.Namespace". É possível ao programador criar um objecto de "formatter" especifico, criando uma classe que implementa este interface. No entanto o mais frequente é utilizar-se um dos objectos que já existem na plataforma .Net.
"Formatter Objects"
A framework disponibiliza os seguintes objectos:
- "BinaryFormatter": Definido no namespace "System.RunTime.Serialization.Formatters.Binary" disponibiliza um mecanismo eficiente para guardar objectos num formato binário compacto. Na verdade é a própria representação binária do objecto em memória que é mantida, por isso os processos de "serialization" e "deserialization" são muito rápidos.
- "SoapFormatter": definido no namespace "System.RunTime.Serialization.Formatters.Soap", guarda os objectos num formato reconhecível por humanos, em XML, seguindo as especificações SOAP ("Simple Object Access Protocol"). Os processos de serialização de deserialização são relativamente mais lentos que através do "BinaryFormatter", no entanto tem a vantagem de permitir o envio de dados entre aplicações através de HTTP, bem como de permitir a visualização dos dados numa forma compreensível.
"Binary Serialization"
Os métodos chave que todos os objectos "Formatter" suportam são "Serialize" e "Deserialize"
O método "Serialize" requer como argumentos um objecto do tipo "Stream" e o objecto a ser serializado.
Para recuperar o objecto, é utilizado o método "Deserialize" que aceita como único argumento o objecto de "Stream" e retorna o objecto original. Este objecto é devolvido no tipo "Object" por isso tem de ser convertido no tipo original.
'Passo 1 - Guardar num ficheiro
'Cria um array de inteiros
Dim Arr() As Integer = {1, 2, 4, 8, 16, 32, 64, 128, 256}
'Cria um Stream para guardar o objecto
Using fs As New FileStream("D:\myfile.dat", FileMode.Create)
'Cria o "Formatter" binário
Dim MyBinaryFormatter As New BinaryFormatter
'Copia o objecto para o ficheiro
MyBinaryFormatter.Serialize(fs, Arr)
End Using
'Passo 2 - Repõe o objecto
Dim ArrCopia() As Integer
'le o ficheiro com o objecto
Using fs2 As New FileStream("D:\myfile.dat", FileMode.Open)
Dim MyBinaryFormatter2 As New BinaryFormatter
'Deseriliza o conteudo do ficheiro
ArrCopia = DirectCast(MyBinaryFormatter2.Deserialize(fs2), Integer())
End Using
Exemplo prático: Conjugando o estudo já efectuado sobre genéricos com a serialização, é possível construir um mecanismo genérico para serialização e deserialização de objectos:
Public Sub SerializeToFile(Of T)(ByVal path As String, ByVal obj As T)
' Cria um Stream para guardar o objecto
Using fs As New FileStream(path, FileMode.Create)
' Cria o "Formatter" binário
Dim bf As New BinaryFormatter()
' Copia o objecto para o ficheiro
bf.Serialize(fs, obj)
End Using
End Sub
Public Function DeserializeFromFile(Of T)(ByVal path As String) As T
' le o ficheiro com o objecto
Using fs As New FileStream(path, FileMode.Open)
Dim bf As New BinaryFormatter()
' Deseriliza o conteudo do ficheiro
Return DirectCast(bf.Deserialize(fs), T)
End Using
End Function
Demo:
"SOAP Serialization"
O "SoapFormatter" esta marcado como obsoleto na "framework" 2.0. Segundo a "Microsoft" deve optar-se sempre que possível pelo "Binary Formatter". Contudo o "Soap Formatter" possui uma capacidade que falta ao "Binary Formatter", a capacidade de serializar os objectos em XML, legível por humanos.
Esta capacidade é muito útil, especialmente durante o tempo de desenvolvimento e "debug". É por isso frequente a utilização deste "formatter" durante o desenvolvimento e passagem para o "Binary Formatter" em produção.
O namespace "System.RunTime.Serialization.Formatters.Soap" pertence ao "assembly" "System.RunTime.Serialization.Formatters.Soap.dll". Este "assembly" não é referenciado automaticamente pelo "visual studio", pelo que é necessário criar a referencia manualmente.
'Cria um hashtable e coloca-lhe alguns dados
Dim MyHash As New Hashtable
MyHash.Add("one", 1)
MyHash.Add("Two", 2)
MyHash.Add("Three", 3)
'Cria um SoapSerializer
Dim MySoapFormatter As New SoapFormatter
'Guarda a hashtable em disco no formato SOAP
Using fs As New FileStream("D:\MyHash.xml", FileMode.Create)
MySoapFormatter.Serialize(fs, MyHash)
End Using
'Recria o objecto utilizando o mesmo SoapFormatter
Dim MyHashCopy As Hashtable
Using fs As New FileStream("D:\MyHash.xml", FileMode.Open)
MyHashCopy = DirectCast(MySoapFormatter.Deserialize(fs), Hashtable)
End Using
Demo:
Criação de tipos serializáveis
Para além da serialização de tipos básicos da plataforma, é possível ao programador criar classes serializáveis.
Para criar uma classe serializável apenas é necessário utilizar um atributo especial: "Serializable":
_
Class Person
'…
End Class
Para que a serialização funcione são necessárias duas condições:
- A classe base, caso exista tem de serializável
- Todos os campos da classe tem de ser de tipos serializáveis.
Atributo "NonSerialized"
O atributo "NonSerialized" indica que um determinado campo de uma classe "serializable" não deve ser serializado. Este atributo deve ser utilizado nos seguintes casos:
Quando uma classe possui um campo de um tipo não serializável (por exemplo o system.windows.form.control), este atributo é obrigatório para que a classe seja serializada.
Este atributo também é obrigatório quando o valor de um campo deixa de ser válido após deserialização (por exemplo apontadores, handles para ficheiros, etc.)
Quando o valor de um campo é facilmente calculável a partir de outros campos, este atributo pode ser utilizado para diminuir a quantidade de informação a serializar.
Private Xpto As Xpto
"Object Graphs"
Um "object graph" é um conjunto de múltiplos objectos com referências entre si.
É possível serializar um "object graph", mesmo que possua referencias circulares.
Dim p1 As New Person("Antonio", "Silva", #1/12/1960#)
Dim p2 As New Person("João", "Ferreira", #3/6/1962#)
Dim p3 As New Person("Ana", "Dias", #10/4/1965#)
'Estabelecer uma referência circular entre duas pessoas
p2.Spouse = p3
p3.Spouse = p2
'Colocar todas as pessoas numa lista de pessoas
Dim List As New List(Of Person)(New Person() {p1, p2, p3})
'Serializa para o disco através dos métodos genéricos do exemplo anterior
Dim MyGenericSerialization As New GenericSerialization
MyGenericSerialization.SerializeToFile("D:\Persons.Dat", List)
'Cria uma segunda lista e obtem os dados
Dim list2 As List(Of Person) = MyGenericSerialization.DeserializeFromFile(Of List(Of Person))("D:\Persons.Dat")
"Custom Serialization"
Quando é necessária uma solução mais complexa para lidar com questões de "serialization" e "deserialization" torna-se necessário desenvolver uma "Custom Serialization".
Uma "Custom Serialization" pode ser útil em casos como:
- Quando é necessário decidir dinamicamente que informação deve ser serializada e mantida.
- Quando é necessário executar código quando o objecto é deserializado, provavelmente para recalcular valores que entretanto deixaram de ser válidos.
Interface IDeserializationCallBack
O caso mais simples de "Custom Serialization" consiste em executar algum código quando o objecto é completamente deserializado.
O exemplo seguinte contempla o seguinte cenário:
- Uma classe "person" abre um ficheiro no construtor, e todos os outros métodos da classe dependem deste ficheiro.
- Quando o objecto é deserializado, o construtor não é executado novamente, como tal se nada for feito, a aplicação irá falhar. A solução é implementar nesta classe o interface "IDeserializationCallBack".
- Este interface possui um único método "OnDeserialization" o qual é invocado pela "framework" quando o objecto é totalmente deserializado.
- É importante notar que a framework invoca o método "OnDeserialization" quando todo o "object graph" estiver sido deserializado, o que significa que garantidamente, todos os objecto dependentes do objecto corrente foram iniciados correctamente. Esta informação é importante se os campos a iniciar no método "OnDeserialization" dependerem de outros campos.
_
Public Class Person
Implements IDeserializationCallback
Implements IDisposable
Private LogStream As FileStream
Public FirstName As String
Public LastName As String
Private Sub OpenLogFile()
Dim fileName As String = Me.FirstName & " " & Me.LastName & ".txt"
LogStream = New FileStream(fileName, FileMode.OpenOrCreate)
End Sub
Sub New(ByVal firstName As String, ByVal lastName As String)
Me.FirstName = firstName
Me.LastName = lastName
' Abre o ficheiro para logging.
OpenLogFile()
End Sub
' Este método é chamado quando o objecto foi completamente deserialized.
Private Sub OnDeserialization(ByVal sender As Object) Implements IDeserializationCallback.OnDeserialization
' ReAbre o ficheiro
OpenLogFile()
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
If LogStream IsNot Nothing Then LogStream.Close()
End Sub
End Class
Interface Iserializable
O interface "ISerializable" permite um controlo total sobre ambos os processos de serialização e deserialização.
Este interface útil em casos em que se pretende definir em tempo de execução quais os campos a serializar ou quando se pretende serializar campos num formato diferente do padrão.
O Interface "ISerializable" expõe apenas um método "GetObjectData" e tem a seguinte sintaxe:
Protected Sub GetObjectData(ByVal info As SerializationInfo, ByVal Context As StreamingContext)
'…
End Sub
O método "GetObjectData" é invocado quando o objecto é passado para o método "Serialize" do "formatter object".
O seu propósito é preencher o objecto "SerializationInfo" com toda a informação do objecto a ser serializado.
O código dentro deste método pode examinar a estrutura "StreamingContext" para obter detalhes acerca do processo de serialização (por exemplo para determinar se o objecto está a ser serializado para um ficheiro ou para memória).
A presença do interface "ISerializable" implica a existência de um construtor especial com a estrutura:
Protected Sub New(ByVal info As SerializationInfo, ByVal Context As StreamingContext)
'…
End Sub
A "framework" chama este construtor quando o objecto é deserializado. A não inclusão deste construtor não gera nenhum erro de compilação, no entanto será gerado um erro de "run time" quando o objecto for deserializado.
O "Scope" utilizado para o método "GetObjectData" e para o construtor é crucial e tem de ser:
- "Protected": Se a classe pode ser base para outras classe derivadas
- "Private": Se a classe for "Sealed".
O scope nunca deve ser "Public", pois sendo "Protected" ou "Private" o tipo tem obrigatóriamente de possuir pelo menos mais um construtor público, para que o tipo possa ser instanciado.
Nota: O objecto "StreamingContext" é um objecto especial que permite associar o objectivo da serialização ao "formatter object" seleccionado. Este objecto é aceite como segundo argumento num construtor "overloaded".
A utilização deste objecto é recomendada e é considerada uma boa prática, pois permite tomar decisões de acordo com o tipo de serialização. Exemplo:
Dim SC As New StreamingContext(StreamingContextStates.file)
Dim BF As New BinaryFormatter(Nothing, SC)
O argumento do método "GetObjectData" constituído pelo objecto "SerializeInfo" funciona como um dicionário que é preenchido com os valores a serem serializados, através do método "AddValue":
Protected Sub GetObjectData(ByVal info As SerializationInfo, ByVal Context As StreamingContext) Implements ISerializable.GetObjectData
'Guarda os campos
info.AddValue("FirstName", Me.FirstName)
info.AddValue("Lastname", Me.LastName)
' …
End Sub
Como esperado, o objecto passado para o método "AddValue" tem de ser "serializable"
Os valores são obtidos posteriormente através do método "GetValue":
Protected Sub New(ByVal info As SerializationInfo, ByVal Context As StreamingContext)
'Obtem os campos
Me.FirstName = CStr(info.GetValue("Firstname", GetType(String)))
Me.LastName = info.GetString("LastNAme")
' …
End Sub