Por vezes existe a necessidade de persistir dados num ficheiro antes do término de uma aplicação. Normalmente, quando enveredamos por linguagens orientadas a objectos como é o caso do java ou do csharp, esses dados encontram-se estruturados em objectos que poderão manter uma série de relações entre si. De modo a possibilitar a persistência de objectos completos num ficheiro ou transmiti-los para uma outra máquina por intermédio de uma rede, ambas as linguagens implementaram aquilo que recebe a designação de serialização e desserialização de objectos. Irei aqui discutir alguns detalhes aferentes a estes conceitos em csharp.
Começo com um objecto simples, SerializableObject que implementa parcialmente uma forma rude de lista com um número máximo de elementos.
public delegate void AddedDescription(object sender, AddEventArgs args);
public class AddEventArgs : EventArgs
{
public string AddedDescription { get; set; }
}
[Serializable]
public class SerializableObject
{
private string[] descriptions;
private string info;
private int current = 0;
public SerializableObject(int count)
{
if (count < 0)
{
throw new ArgumentException("Parameter count must be positive.");
}
this.descriptions = new string[count];
}
public int Length
{
get
{
return this.current;
}
}
public string AddicionalInfo
{
get
{
return this.info;
}
set
{
this.info = value;
}
}
[field: NonSerialized]
public event AddedDescription DescriptionAdded;
public void Add(string description)
{
if (this.current >= this.descriptions.Length)
{
throw new Exception("Can not add more objects.");
}
if (this.DescriptionAdded != null)
{
this.DescriptionAdded.Invoke(this, new AddEventArgs() { AddedDescription = description });
}
this.descriptions[this.current++] = description;
}
public override string ToString()
{
StringBuilder result = new StringBuilder();
result.Append("{");
result.Append(this.info);
result.Append(", [");
if (this.current > 0)
{
result.Append(this.descriptions[0]);
}
for (int i = 1; i < this.current; ++i)
{
result.Append(", ");
result.Append(this.descriptions[i]);
}
result.Append("]");
result.Append("}");
return result.ToString();
}
}
Observamos que a classe SerializableObject está marcada com o atributo [Serializable]. Este atributo permite estabelecer o facto de que um objecto deste tipo admite serialização. O código que se segue serializa um objecto para um MemoryStream e volta a desserializá-lo, isto é, a recuperá-lo novamente.
class Program
{
static void Main(string[] args)
{
try
{
SerializableObject objectToSerialize = new SerializableObject(10) { AddicionalInfo = "initial" };
objectToSerialize.DescriptionAdded += AddDescriptionVerifier;
objectToSerialize.Add("string 1");
objectToSerialize.Add("string 2");
objectToSerialize.Add("string 3");
objectToSerialize.Add("string 4");
Console.WriteLine("Before serialization:");
Console.WriteLine(objectToSerialize);
var myStream = new MemoryStream();
var formatter = new BinaryFormatter();
formatter.Serialize(myStream, objectToSerialize);
myStream.Seek(0, SeekOrigin.Begin);
var deserializedObject = (SerializableObject)formatter.Deserialize(myStream);
Console.WriteLine("After serialization and desserialization:");
Console.WriteLine(deserializedObject);
deserializedObject.Add("invalid");
}
catch (Exception except)
{
Console.WriteLine(except.Message);
}
}
static void AddDescriptionVerifier(object sender, AddEventArgs args)
{
if (string.IsNullOrEmpty(args.AddedDescription))
{
throw new Exception("Can't add nulls.");
}
if (args.AddedDescription.ToLower() == "invalid")
{
throw new Exception("Invalid add element.");
}
}
}
Este requer a utilização do namespace System.Runtime.Serialization.Formatters.Binary. Existe aqui um pormenor que carece de especial atenção. A classe SerializableObject atira um evento sempre que o utilizador adiciona um item à lista. No programa, por seu turno, está implementado um listener que reage com uma excepção quando tentamos introduzir a palavra “invalid”. Verificamos que, ao executar a pequena aplicação, quando tentamos introduzir a palavra inválida no objecto desserializado, desserializedObject, o listener correspondente é executado. Contudo, podemos querer desserializar o objecto num contexto sob o qual não existe esse listener. Neste caso, somos forçados a colocar sobre o evento, SerializableObject::DescriptionAdded, o atributo [field: NonSerialized]. Este atributo permite indicar que o evento não deverá ser serializado. A palavra field que precede o verdadeiro atributo, [NonSerialized], permite instruir o compilador a considerar o evento como um campo, uma vez que o atributo é apenas válido nesse caso. Obviamente, qualquer campo marcado com [NonSerialized], não é serializado. Para serializar o objecto objectToSerialize, recorremo-nos da instância de um formatador binário. Este formatador permite serializar o objecto em formato binário. Porém, também existe um formatador que permite serializar objectos em XML.
Suponhamos agora que, sempre que desserializamos o objecto, o seu campo info terá de apresentar a palavra “deserialized”. Facilmente conseguimos este efeito se a nossa classe serializável implementar a interface IDeserializationCallback, como é mostrado de seguida.
[Serializable]
public class SerializableObject : IDeserializationCallback
{
// O resto do código aferente à classe segue aqui
void IDeserializationCallback.OnDeserialization(object sender)
{
this.info = "deserialized";
}
}
Imaginemos um terceiro cenário no qual existe uma classe com um número elevado de campos que aglomera toda a estrutura relativa ao nosso modelo de dados. No entanto, pretendemos descartar campos fazendo essa escolha em tempo de execução. Neste caso, o atributo [NonSerialized] torna-se impotente, uma vez que apenas pode ser alterado em tempo de compilação.
De modo a responder ao problema, somos conduzidos àquilo que recebe o nome de Surrogates (delegados). No caso da serialização, um Surrogate é uma classe que implementa a interface ISerializationSurrogate e permite gerir a informação que é serializada. Esta interface implementa duas funções que lidam com o objecto a tratar, um SerializationInfo que contém a informação a ser serializada, um StreamingContext que permite definir um contexto de serialização e um ISurrogateSelector. Esta última classe permite percorrer uma série de Surrogates de modo a encontrar aquele que se adequa ao tratamento de um dado objecto. O código que se segue implementa o nosso Surrogate.
class MySerializationSurrogate<ObjectType> : ISerializationSurrogate
where ObjectType : class
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
List<string> listInContex = context.Context as List<string>;
if (listInContex == null)
{
throw new SerializationException("There are no defined fields in context to be serialized.");
}
foreach (var item in listInContex)
{
var serializable = typeof(ObjectType).GetField(item, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj);
info.AddValue(item, serializable);
}
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
List<string> listInContex = context.Context as List<string>;
if (listInContex == null)
{
throw new SerializationException("There are no defined fields in context to be deserialized.");
}
foreach (var item in listInContex)
{
var property = typeof(ObjectType).GetField(item, BindingFlags.Instance | BindingFlags.NonPublic);
var deserialized = info.GetValue(item, property.FieldType);
property.SetValue(obj, deserialized);
}
return null;
}
}
Aqui estão implementadas as duas funções, GetObjectData que colecciona os dados dos campos do objecto a serializar e inclui no SerializationInfo, o qual realiza a serialização dessa informação. A lista dos campos a serializar é incluída no respectivo StreamingContext. Por seu turno, a função SetObjectData permite obter, com base no contexto, os campos que são para desserializar e reconstrói o objecto preenchendo apenas esses campos (os restantes ficam nulos). Existe uma sobrecarga do construtor da classe BinaryFormatter que permite receber o contexto e o selector. Ao selector, adicionamos o nosso Surrogate e ao contexto adicionamos a lista dos campos que queremos serializar ou desserializar.
static void Main(string[] args)
{
SerializableObject objectToSerialize = new SerializableObject(10) { AddicionalInfo = "initial" };
objectToSerialize.DescriptionAdded += AddDescriptionVerifier;
objectToSerialize.Add("string 1");
objectToSerialize.Add("string 2");
objectToSerialize.Add("string 3");
objectToSerialize.Add("string 4");
Console.WriteLine("Before serialization:");
Console.WriteLine(objectToSerialize);
var myStream = new MemoryStream();
SurrogateSelector surrogateSelector = new SurrogateSelector();
var context = new StreamingContext(StreamingContextStates.All, new List<string>(){"info", "current"});
MySerializationSurrogate<SerializableObject> surrogate = new MySerializationSurrogate<SerializableObject>();
surrogateSelector.AddSurrogate(typeof(SerializableObject), context, surrogate);
BinaryFormatter formatter = new BinaryFormatter(surrogateSelector, context);
formatter.Serialize(myStream, objectToSerialize);
surrogateSelector = new SurrogateSelector();
context = new StreamingContext(StreamingContextStates.All, new List<string>() { "info", "current" });
surrogate = new MySerializationSurrogate<SerializableObject>();
surrogateSelector.AddSurrogate(typeof(SerializableObject), context, surrogate);
formatter = new BinaryFormatter(surrogateSelector, context);
myStream.Seek(0, SeekOrigin.Begin);
var deserializedObject = (SerializableObject)formatter.Deserialize(myStream);
Console.WriteLine("After serialization and desserialization:");
Console.WriteLine(deserializedObject);
}
A execução do código anterior resulta numa excepção provocada pela tentativa de aceder a um campo por intermédio de uma referência nula. De facto, como serializamos e desserializamos apenas os campos info e current, o campo description fica a nulo. No entanto, este é acedido na função ToString onde é lançada a excepção. Podemos, porventura, precaver esta situação na função IDeserializationCallback.OnDeserialization onde forçamos os campos nulos a adquirir um valor por defeito, mantendo a integridade do objecto.