Serialização de Objetos & Persistência em Arquivos

Qualidade: 

Estrela ativaEstrela ativaEstrela ativaEstrela ativaEstrela ativa
 

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

Arquivos anexados
ArquivoDescriçãoTamanhoModificado em
Download this file (OBJSER.zip)OBJSERUm pequeno projeto que demonstra o uso da classe TObjectFile, a qual permite serializar objetos que dela descendem5 KB02/09/2016 às 01:15
e-max.it: your social media marketing partner
Marcadores
DFM Resource RTTI
Ajude nosso site visitando nossos patrocinadores!

Temos 91 visitantes e Nenhum membro online nos últimos 10 minutos (9.1 visitantes por minuto).