Serialização de Objetos & Persistência em Arquivos
Escrito por Carlos B. Feitoza Filho | |
Categoria: Artigos | |
Categoria Pai: Addicted 2 Delphi! | |
Acessos: 8502 |
Enquanto está na memória, um objeto é simplesmente uma sequência de bytes, uma estrutura de dados binários. A serialização consiste em pegar todos estes bytes que representam o objeto e seus dados na memória e obter uma representação que possa ser facilmente portada. Normalmente a serialização gera dados em texto plano, que pode ser lido facilmente por humanos e esta seria a forma mais tradicional de serialização, contudo a outra característica da serialização, a portabilidade, faz com que mesmo se salvando os dados binários, desde que estes sejam de alguma forma portáveis, haja uma serialização, portanto, serialização, grosso modo, é sinônimo de portabilidade para objetos.
RTTI: A base da serialização no Delphi
RTTI significa Runtime Type Information, ou seja, Informações de Tipo em Tempo de Execução, mas o que isso tem a ver com a serialização? Na verdade tem tudo a ver! No Delphi os objetos que contém membros na seção published, automaticamente geram RTTI para tais membros e o mecanismo usado para serializar um objeto só é viável por conta do RTTI. Membros que possuem RTTI exportam em tempo de execução o seu tipo de dado além do dado propriamente dito e assim é possível, por meio de métodos específicos, perguntar a um objeto qual o tipo de uma de suas propriedades e, baseando-se na resposta, é possível formatar uma saída serializada que posteriormente pode ser convertida de volta no objeto sem qualquer esforço.
Na forma como vou apresentar aqui neste artigo, toda essa conversão e obtenção de tipos de dados, bem como a geração de saída serializada, será feita pelo próprio Delphi! Nenhuma linha de código adicional será necessária porque o Delphi já serializa constantemente um de seus mais importantes objetos, os formulários.
Se você não acredita em mim, abra agora um projeto vazio no Delphi e no TForm que vai aparecer, coloque um TButton, um TLabel e um TEdit. Altere algumas das propriedades do TForm e destes controles. Veja abaixo o TForm que eu criei como exemplo:
Agora clique com o botão direito do mouse em uma área vazia do TForm e clique em View As Text. A seguir está o que aparece ao clicar em View As Text:
object FORM1: TFORM1
Left = 0
Top = 0
BorderIcons = [biSystemMenu]
Caption = 'Salvar nome'
ClientHeight = 57
ClientWidth = 215
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
Icon.Data = {
0000010001001010100001000400280100001600000028000000100000002000
0000010004000000000000000000000000000000000010000000000000000101
01000C88160037C9630067EB8F00299D310045DB75002586280059E082002692
2D008AF5A6003FD46E0012911B0078F19A005AE8870024AA31004DDF7B000000
0000000000000000081111600000000006FA226000000000087A226000000000
0875A260000008888B75A21666600ED555F55A2222100ECDDDDFF55A22100EC3
3DDFFF55A2100E9999CDDF737FB004444B9DD5B8666000000493DA8000000000
0493358000000000049CC7800000000004EEEE80000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000}
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object LABE1: TLabel
Left = 8
Top = 8
Width = 27
Height = 13
Caption = 'Nome'
end
object BUTN1: TButton
Left = 132
Top = 27
Width = 75
Height = 21
Caption = 'Salvar'
TabOrder = 0
OnClick = BUTN1Click
end
object EDIT1: TEdit
Left = 8
Top = 27
Width = 121
Height = 21
TabOrder = 1
Text = 'Carlos'
end
end
O que vai ser exibido para você pode ser ligeiramente diferente. Mas o que é isso? Isso é o TForm serializado em forma de texto legível e um mecanismo de stream carrega esta definição de uma vez só, toda vez que um TForm é criado, e seta todas as propriedades dele e de todos os controles que são de sua propriedade (tem TForm como Owner). Note o quão variados são os tipos das informações salvas. Neste exemplo básico você vê, números inteiros, strings, a atribuição de um manipulador de evento (na linha 42), um conjunto com um valor (na linha 4), um conjunto vazio (na linha 13), constantes (nas linhas 9 e 10) e por fim, mas não menos importante, um valor binário completo, que representa o ícone do TForm (na linha 14). Sofisticado, não é?
Como seria legal se eu pudesse salvar meus arquivos de configuração nesse formato e com esta facilidade de carregamento instantâneo não é? Yes! You Can! Continue lendo ;)
Como salvar objetos completos como arquivos
Quando se usa arquivos INI ou mesmo o registro do Windows, precisamos lidar com funções específicas para salvamento de dados. É necessário, normalmente, criar uma instância de um objeto manipulador, TIniFile por exemplo, e informar o nome do arquivo, suas seções, suas variáveis e seus respectivos valores. Tudo sendo salvo como String e muitas vezes ocupando mais espaço do que o necessário.
Ao carregar estes dados precisaríamos fazer todo o procedimento oposto e, dependendo do caso, ainda precisaríamos fazer conversões de dados (String para TDateTime por exemplo). Outra enorme desvantagem dos arquivos INI é que no mesmo não é possível salvar textos mais complexos com quebras de linha, muito menos dados binários. A serialização de um objeto e sua posterior persistência em um arquivo resolve todos os problemas de falta de suporte em arquivos INI e registro do Windows e habilita o programador a salvar QUALQUER TIPO DE INFORMAÇÃO num arquivo.
String, Integer, Currency, Double, Float, TDateTime, Extended, conjuntos de dados constantes, constantes, além de coleções de tipos customizados e até mesmo dados binários crus (imagens, ícones, executáveis, dlls). Praticamente qualquer coisa pode ser salva no arquivo. O que você acharia de salvar e carregar um arquivo dessa forma?:
procedure SalvarConfiguracoes;
begin
Configuracoes.SaveText;
end;
procedure CarregarConfiguracoes;
var
FileName: TFileName;
begin
FileName := ChangeFileExt(ParamStr(0),'.config');
Configuracoes.LoadFromTextFile(FileName);
if not FileExists(FileName) then
Configuracoes.SaveText;
end;
Os dois procedures acima já salvam e carregam todas as configurações contidas no objeto "Configurações" de uma só vez! Eu não sei você caro leitor, mas quando eu vi isso pela primeira vez eu achei genial :). Chega de falar, vamos ao que interessa. Primeiramente a unit "mágica" que torna tudo isso possível:
unit UObjectFile;
interface
uses
Classes, SysUtils;
type
TObjectFile = class(TComponent)
private
FFileName: String;
public
constructor Create(POwner: TComponent); override;
destructor Destroy; override;
procedure LoadFromTextFile(const aFileName: TFileName);
procedure SaveToTextFile(const PFileName: TFileName);
procedure SaveText;
function ToString: String; override;
procedure FromString(const aTextualRepresentation: String);
end;
implementation
{ TObjectFile }
procedure TObjectFile.SaveText;
begin
if FFileName <> '' then
SaveToTextFile(FFileName);
end;
procedure TObjectFile.SaveToTextFile(const PFileName: TFileName);
begin
with TStringList.Create do
try
Text := Self.ToString;
SaveToFile(PFileName);
finally
Free;
end;
end;
constructor TObjectFile.Create(POwner: TComponent);
begin
inherited;
FFileName := '';
end;
destructor TObjectFile.Destroy;
begin
SaveText;
inherited;
end;
procedure TObjectFile.LoadFromTextFile(const aFileName: TFileName);
begin
FFileName := aFileName;
if FileExists(FFileName) then
with TStringList.Create do
try
LoadFromFile(FFileName);
FromString(Text);
finally
Free;
end;
end;
{$HINTS OFF}
procedure TObjectFile.FromString(const aTextualRepresentation: String);
var
BinStream: TMemoryStream;
StrStream: TStringStream;
begin
StrStream := TStringStream.Create(aTextualRepresentation);
try
BinStream := TMemoryStream.Create;
try
StrStream.Seek(0, sofrombeginning);
ObjectTextToBinary(StrStream, BinStream);
BinStream.Seek(0, sofrombeginning);
Self := BinStream.ReadComponent(Self) as TObjectFile;
finally
BinStream.Free
end;
finally
StrStream.Free;
end;
end;
{$HINTS ON}
function TObjectFile.ToString: String;
var
BinStream: TMemoryStream;
StrStream: TStringStream;
S: string;
begin
inherited;
BinStream := TMemoryStream.Create;
try
StrStream := TStringStream.Create(S);
try
BinStream.WriteComponent(Self);
BinStream.Seek(0, sofrombeginning);
ObjectBinaryToText(BinStream, StrStream);
StrStream.Seek(0, sofrombeginning);
Result := StrStream.DataString;
finally
StrStream.Free;
end;
finally
BinStream.Free
end;
end;
end.
Como se pode ver, esta unit não tem muito código. Ela contém apenas wrappers e facilitadores para as funções ObjectBinaryToText e ObjectTextToBinary. São estas as funções mágicas de fato, as quais manipulam os membros published do objeto fazendo uso do RTTI.
Para usar esta unit e a classe TObjectFile é preciso primeiro criar uma classe com nossos membros que deverão ser serializados. Como estamos usando como exemplo um arquivo de configurações, nada mais justo do que criar uma classe TConfiguracoes. A unit completa está abaixo:
unit UConfiguracoes;
interface
uses
UObjectFile;
type
TValorEnum = (veItem0, veItem1, veItem2, veItem3);
TValorSet = set of TValorEnum;
TConfiguracoes = class (TObjectFile)
private
FValorCurrency: Currency;
FValorInteger: Integer;
FValorDouble: Double;
FValorDateTime: TDateTime;
FValorString: String;
FValorBoolean: Boolean;
FValorEnum: TValorEnum;
FValorSet: TValorSet;
published
property ValorInteger: Integer read FValorInteger write FValorInteger default 0;
property ValorCurrency: Currency read FValorCurrency write FValorCurrency;
property ValorDouble: Double read FValorDouble write FValorDouble;
property ValorDateTime: TDateTime read FValorDateTime write FValorDateTime;
property ValorString: String read FValorString write FValorString;
property ValorBoolean: Boolean read FValorBoolean write FValorBoolean default False;
property ValorEnum: TValorEnum read FValorEnum write FValorEnum default veItem0;
property ValorSet: TValorSet read FValorSet write FValorSet default [];
end;
implementation
end.
Esta unit é ainda menor, como esperado, já que todo trabalho duro é feito por TObjectFile. Incluí na classe TConfiguracoes vários membros de tipos de dados comuns. Eu poderia ter colocado um membro binário, mas isso complicaria na hora de atribuir um valor a ele, portanto apenas acredite que seria perfeitamente possível incluir um membro do tipo TImage, por exemplo. Os dados binários da imagem seriam gravados sem qualquer problema. Outras coisas que poderiam existir nesta classe seriam subclasses, incluindo coleções. Se você já criou classes com subclasses, isto é, membros que são de tipos de outras classes, bastaria incluir tais membros e inicializá-los corretamente que os dados da subclasse apareceriam no objeto serializado.
Note que algumas das propriedades possuem a cláusula default. Essa cláusula instrui as funções ObjectBinaryToText e ObjectTextToBinary que se o valor da propriedade for igual àquele valor default ele não será incluído no objeto final serializado. Por exemplo, ValorInteger tem um valor default = 0. Na hora de serializar o objeto, caso ValorInteger = 0, ele não vai figurar na serialização, porque não é necessário. Isso é bom para economizar espaço no arquivo que vai receber o objeto serializado, pois ele não conterá referências a membros cujos valores são iguais aos seus respectivos valores default. Apesar do valor default ser importante para economizar espaço, infelizmente apenas membros dos tipos integer, boolean, enum e set podem ter valores default, em suma, apenas ordinais e conjuntos podem ter valores padrão, mesmo assim eu considero boa prática usá-los.
Explore o exemplo anexado a este artigo para um melhor entendimento