RTTI (Run-time Type Information)

By: Rodrigo Leonhardt

Abstract: Como utilizar a nova RTTI para descobrir dinamicamente sobre classes e objetos no Delphi

    Introdução

Você já se perguntou como o Object Inspector da IDE do Delphi conhece as propriedades e eventos de um objeto? E como um arquivo texto (DFM) resulta em um formulário com componentes bem definidos, em tempo de execução?

Mesmo que pareça, não é mágica! É o uso intenso de um recurso chamado RTTI (Run-time Type Information) disponível desde sempre nas versões do Delphi.

Apesar de presente, este recurso não era devidamente documentado e poucos eram os “curiosos” que descobriam esse tesouro. A partir do Delphi 2010 a RTTI recebeu a atenção que merecia e foi reformulada, incrementada e documentada!

O conceito é conhecido em outras linguagens como reflexão (reflection) e consiste numa variedade de informações geradas pelo compilador acerca dos tipos (classes), que podem ser consultadas e alteradas em tempo de execução.

Podemos classificar essas informações como metadados que descrevem as características dos tipos, como propriedades, métodos e etc.

Sem demagogias ou blá blá blá, neste artigo você encontrará noções da RTTI e exemplos práticos de como utilizá-la em seu favor.

    Utilizando a Rtti

    TRttiContext

O primeiro passo é adquirir um contexto. Um objeto TRttiContext está para a iteração com tipos, assim como um TSQLConnection para a iteração com banco de dados.

Esta classe funciona como um canal, gerenciando objetos em memória e facilitando o acesso a informações sobre tipos em tempo de execução.

Para descobrir informações de um tipo, utilizamos o contexto conforme o código abaixo.

procedure Exemplo1;
var
  ctxRtti : TRttiContext;
begin

  ctxRtti := TRttiContext.Create;

  try
    ctxRtti.GetType(TForm);

    ctxRtti.FindType('Forms.TForm');
  finally
    ctxRtti.Free;
  end;

end;

O primeiro ponto a ser observado é que o contexto deve ser criado antes de qualquer consulta, e destruído no fim. Isso garante que os objetos instanciados para a consulta serão corretamente destruídos.

Os principais métodos desta classe são GetType e FindType. Enquanto o método GetType utiliza uma classe explícita, o método FindType utiliza um nome completo (Qualified Name) para expor os dados do tipo, através do objeto TRttiType.

    TRttiType

A classe TRttiType dispõe de métodos para pesquisar características do tipo como: propriedades, campos, métodos e atributos.

Para consultar os métodos de um tipo, utilizamos a função GetMethods, que retorna um Array de TRttiMethod.

Também há o método GetMethod, que retorna apenas o método com o nome informado.

O código abaixo lista o nome de todos os métodos da classe TForm:

procedure Exemplo2(pMemo : TMemo);
var
  ctxRtti : TRttiContext;
  typRtti : TRttiType;
  metRtti : TRttiMethod;

begin

  ctxRtti := TRttiContext.Create;

  try
    typRtti := ctxRtti.GetType(TForm);

    for metRtti in typRtti.GetMethods do
      pMemo.Lines.Add(metRtti.Name);

    metRtti := typRtti.GetMethod('CLOSE'); //Apenas para constar
  finally
    ctxRtti.Free;
  end;

end;

Hide image

Para consultar campos (fields) de um tipo, podemos utilizar a função GetFields, que retorna um Array de TRttiField.

Já o método GetField, retorna apenas o campo com o nome informado.

O código abaixo lista o nome e o tipo de todos os campos da classe TButton.

procedure Exemplo3(pMemo : TMemo);
var
  ctxRtti : TRttiContext;
  typRtti : TRttiType;
  fldRtti : TRttiField;

begin

  ctxRtti := TRttiContext.Create;

  try
    typRtti := ctxRtti.GetType(TButton);

    for fldRtti in typRtti.GetFields do
      pMemo.Lines.Add(fldRtti.Name +':'+ fldRtti.FieldType.ToString);

    fldRtti := typRtti.GetField('FDefault'); //Apenas para constar
  finally
    ctxRtti.Free;
  end;

end;

Hide image

Para consultar propriedades de um tipo, utilize a função GetProperties, que retorna um Array de TRttiProperty.

Já o método GetProperty, retorna apenas a propriedade com o nome especificado.

O código abaixo lista o nome e o tipo de todas as propriedades da classe TLabel.

procedure Exemplo4(pMemo : TMemo);
var
  ctxRtti : TRttiContext;
  typRtti : TRttiType;
  prpRtti : TRttiProperty;

begin

  ctxRtti := TRttiContext.Create;

  try
    typRtti := ctxRtti.GetType(TLabel);

    for prpRtti in typRtti.GetProperties do
      pMemo.Lines.Add(prpRtti.Name +':'+ prpRtti.PropertyType.ToString);

    prpRtti := typRtti.GetProperty('caption'); //Apenas para constar
  finally
    ctxRtti.Free;
  end;

end;

Hide image

Para consultar os atributos de um tipo, devemos utilizar a função GetAttributes, que retorna um Array de TCustomAttribute.

Mais adiante, veremos detalhes sobre atributos e suas utilidades.

Para tornar os exemplos a seguir mais realistas, consideraremos a seguinte classe TCliente:

  TCliente = class
  private
    FCodigo: Integer;
    FNome: String;
    FIdade: Integer;
    FSobreNome: String;
  public
    property Codigo : Integer read FCodigo write FCodigo;
    property Nome : String read FNome write FNome;
    property SobreNome : String read FSobreNome write FSobreNome;
    property Idade : Integer read FIdade write FIdade;

    function NomeCompleto : String;

    procedure AlteraCodigo(pNovoCodigo : Integer);
  end;

procedure TCliente.AlteraCodigo(pNovoCodigo: Integer);
begin
  FCodigo := pNovoCodigo;
end;

function TCliente.NomeCompleto: String;
begin
  Result := FNome + ' ' + FSobreNome;
end;

Considere também que antes do início dos exemplos, instanciamos um objeto TCliente da seguinte forma:

  pCliente := TCliente.Create;
  pCliente.Codigo    := 1;
  pCliente.Nome      := 'Rodrigo';
  pCliente.SobreNome := 'Leonhardt';
  pCliente.Idade     := 9;

    TRttiProperty

Em posse de um objeto TRttiProperty podemos extrair preciosas informações acerca de propriedades de tipos, como: Nome (Name), Tipo (PropertyType) e Escopo de Visibilidade (Visibility). Podemos inclusive ler valores (GetValue) e definir valores (SetValue) de certa propriedade de um objeto.

No código abaixo listamos todas as propriedades do objeto TCliente, previamente instanciado, exibindo seus respectivos nomes, tipos e valores.

procedure Exemplo5(pMemo : TMemo; pCliente : TCliente);
var
  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  propRtti : TRttiProperty;

begin

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType( pCliente.ClassType );

  for propRtti in typeRtti.GetProperties do
    pMemo.Lines.Add(propRtti.Name+':'+ propRtti.PropertyType.ToString +'='+ propRtti.GetValue(pCliente).ToString);

  ctxRtti.Free;

end;

Hide image

Ao utilizar o método GetValue (consultar valor) ou o método SetValue (definir valor) é necessário informar como parâmetro a instância do objeto em questão.

Para alterar a Idade do objeto TCliente, poderíamos utilizar o código abaixo:

procedure Exemplo6(pCliente : TCliente);
var
  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  propRtti : TRttiProperty;

begin

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType( pCliente.ClassType );
  propRtti := typeRtti.GetProperty('Idade');

  propRtti.SetValue(pCliente, 15);

  ctxRtti.Free;

end;

Os mesmos conceitos apresentados nesta seção aplicam-se a objetos da classe TRttiField.

    TRttiMethod

Usando objetos da classe TRttiMethod, entre outras coisas, podemos descobrir: Nome do método (Name), Tipo do Retorno (ReturnType), Escopo de Visibilidade (Visibility) e seus parâmetros (GetParameters). Além disso, também é possível invocar seus métodos dinamicamente (Invoke).

O código abaixo lista os métodos e respectivos parâmetros, da classe TCliente.

procedure Exemplo7(pMemo : TMemo; pCliente : TCliente);
var
  ctxRtti   : TRttiContext;
  typeRtti  : TRttiType;
  metRtti   : TRttiMethod;
  paramRtti : TRttiParameter;

begin

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType( pCliente.ClassType );

  for metRtti in typeRtti.GetMethods do
  begin

    //Ignora métodos que não foram implementados diretamente em TCliente 
    if metRtti.Parent.Name <> 'TCliente' then
      Continue;

    pMemo.Lines.Add('Método ' + metRtti.Name);

    if metRtti.ReturnType <> Nil then //Função
      pMemo.Lines.Add(' Retorno ' + metRtti.ReturnType.ToString)
    else //Procedure
      pMemo.Lines.Add(' Retorno ' + '-');

    pMemo.Lines.Add(' Parâmetros');

    for paramRtti in metRtti.GetParameters do
      pMemo.Lines.Add('   ' + paramRtti.Name +':' + paramRtti.ParamType.ToString);

    pMemo.Lines.Add('');
  end;

  ctxRtti.Free;

end;

Hide image

Utilizando as informações de um objeto TRttiMethod, invocar dinamicamente um método é uma tarefa fácil.

O código a seguir ilustra como invocar o método TCliente.AlteraCodigo, utilizando parâmetros.

procedure Exemplo8(pCliente : TCliente);
var
  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  metRtti  : TRttiMethod;

  aParams  : Array of TValue;

begin

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType( tmpCliente.ClassType );
  metRtti  := typeRtti.GetMethod('alteracodigo');

  //Define que o array possui apenas 1 parâmetro
  SetLength(aParams, 1);
  aParams[0] := 20;

  metRtti.Invoke(tmpCliente, aParams);

  ctxRtti.Free;

end;

O método Invoke possui dois parâmetros. O primeiro deles é a instância do objeto, que possui o método a ser invocado. O segundo é um array de valores, que serão utilizados como parâmetros para o método invocado.

Neste exemplo o método AlteraCodigo é invocado e o valor do campo FCodigo passa de 1 para 20.

    Exemplos

E onde vou usar isso tudo? Estes conceitos e técnicas são muito úteis para a construção de frameworks, como um mapeamento objeto-relacional (ORM), arquiteturas de aplicações multicamadas ou até mesmo o DataSnap.

Mas também podemos utilizá-los em menor escala e funcionalidades cotidianas.

    Exemplo Prático 1

Imagine que sua tarefa seja gerar um XML com as informações de uma lista de clientes (objetos TCliente).

Utilizando RTTI, o código abaixo seria uma solução.

procedure Exemplo9(pObjetos : TList; pFileName : String);
var
  txtFile : TextFile;

  tmpObjeto  : TObject;

  i : Integer;

  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  propRtti : TRttiProperty;

begin

  ctxRtti  := TRttiContext.Create;

  AssignFile(txtFile, pFileName);
  Rewrite(txtFile);

  Writeln(txtFile, '<?xml version="1.0" encoding="ISO-8859-1"?>');
  Writeln(txtFile, '<objetos>');

  for i:=0 to pObjetos.Count-1 do
  begin
    tmpObjeto := pObjetos.Items[i];

    Writeln(txtFile, '<objeto>');

    typeRtti := ctxRtti.GetType(tmpObjeto.ClassType);

    for propRtti in typeRtti.GetProperties do
    begin
      Write(txtFile, '<'+ propRtti.Name +'>');
      Write(txtFile, propRtti.GetValue(tmpObjeto).ToString);
      Writeln(txtFile, '</'+ propRtti.Name +'>');
    end;

    Writeln(txtFile, '</objeto>');
  end;

  Writeln(txtFile, '</objetos>');

  CloseFile(txtFile);

  ctxRtti.Free;

end;

Hide image
Click to see full-sized image

Repare que no código acima, não citamos a classe TCliente ou propriedades explícitas em nenhum trecho. Isso permite que você exporte no formato XML listas de absolutamente qualquer tipo de objeto! Inclusive, tipos de objetos diferentes na mesma lista.

    Exemplo Prático 2

Como transmitir um objeto para uma segunda aplicação? Uma das soluções é a seguinte: O remetente deve serializar o objeto desejado, transformando-o num tipo elementar, como uma string. O remetente envia a string para o destinatário, que realiza o processo inverso, transformando a string recebida em um objeto em memória.

O processo de serializar o objeto, responsável pelo remetente, seria semelhante ao observado no exemplo anterior. Mas como realizar o processo inverso? Como transformar um texto em um objeto?

O código abaixo apresenta um exemplo rudimentar desta ação.

procedure TForm1.btnCarregaObjetoClick(Sender: TObject);
begin
  ShowMessage(Exemplo10(GetObjeto).NomeCompleto);
end;

function GetObjeto : TStrings;
begin
  Result := TStringList.Create;
  Result.Add('codigo=5');
  Result.Add('nome=Rodrigo');
  Result.Add('sobrenome=Leonhardt');
  Result.Add('idade=7');
end;

function Exemplo10(pCliente : TStrings) : TCliente;
var
  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  propRtti : TRttiProperty;

  i : Integer;

begin

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType(TCliente);

  Result := TCliente.Create;

  for i:=0 to pCliente.Count-1 do
  begin
    propRtti := typeRtti.GetProperty(pCliente.Names[i]);

    case propRtti.PropertyType.TypeKind of
      tkInteger : propRtti.SetValue(Result, StrToInt(pCliente.ValueFromIndex[i]));
      tkFloat   : propRtti.SetValue(Result, StrToFloat(pCliente.ValueFromIndex[i]));
      else        propRtti.SetValue(Result, pCliente.ValueFromIndex[i]);
    end;

  end;

  ctxRtti.Free;

end;

O parâmetro pCliente do método Exemplo10 provém do método GetObjeto, e corresponde a um objeto TStrings contendo pares (nome=valor) que representam as propriedades de um objeto serializado.

O método Exemplo10 instancia um novo objeto TCliente e percorre a lista de strings associando o valor à propriedade de nome correspondente.

Como resultado do método btnCarregaObjetoClick, uma caixa de mensagem é exibida com o conteúdo “Rodrigo Leonhardt”.

    Atributos

Um recurso muito interessante que foi viabilizado pela nova arquitetura da RTTI é o uso de atributos. Basicamente, um atributo permite que você defina informações adicionais sobre um método, propriedade ou classe que vão além das pré-existentes.

O primeiro passo para utilizar um atributo é implementar uma classe descendente de TCustomAttribute que armazene as informações desejadas.

No código abaixo, três classes são definidas. A classe base TValidacao será a ancestral comum para todas as classes de validação utilizadas no exemplo. A classe TValidacao_NotNull verificará se uma string está em branco, enquanto a classe TValidacao_RangeValues verificará se um valor inteiro está entre um intervalo válido.

Depois de realizada a validação especializada nas classes descendentes, o método AfterValidar é invocado na classe base, que exibirá uma caixa de mensagem com o alerta, caso o valor não seja válido.

TValidacao = class(TCustomAttribute)
  private
    FMensagem : String;

    procedure AfterValidar(pValido : Boolean);
  public
    function Validar(pValue : TValue) : Boolean; virtual;

    constructor Create(pMensagem : String);
  end;

  TValidacao_NotNulll = class(TValidacao)
  public
    function Validar(pValue : TValue) : Boolean; override;
  end;

  TValidacao_RangeValues = class(TValidacao)
  private
    FMax: Integer;
    FMin: Integer;
  public
    constructor Create(pMin, pMax : Integer; pMensagem : String); reintroduce;

    function Validar(pValue : TValue) : Boolean; override;
  end;

constructor TValidacao_RangeValues.Create(pMin, pMax: Integer; pMensagem : String);
begin
  inherited Create(pMensagem);

  FMin := pMin;
  FMax := pMax;
end;

function TValidacao_NotNulll.Validar(pValue: TValue): Boolean;
begin
  Result := pValue.AsString <> '';

  AfterValidar(Result);
end;

procedure TValidacao.AfterValidar(pValido: Boolean);
begin

  if (not pValido) then
    ShowMessage(FMensagem);

end;

constructor TValidacao.Create(pMensagem: String);
begin
  FMensagem := pMensagem;
end;

function TValidacao.Validar(pValue : TValue): Boolean;
begin
  Result := True;

  AfterValidar(Result);
end;

function TValidacao_RangeValues.Validar(pValue: TValue): Boolean;
begin

  Result := True;

  if (pValue.AsInteger < FMin) then
    Result := False;
  if (pValue.AsInteger > FMax) then
    result := False;

  AfterValidar(Result);

end;

O segundo passo é aplicar estes atributos a um elemento. Neste exemplo, considerando a classe TCliente, aplicaremos a validação TValidacao_NotNulll à propriedade SobreNome e a TValidacao_RangeValues à propriedade Idade.

Realizamos esta tarefa incluindo sobre a propriedade desejada a seguinte notação:

[<classe_atributo>(<parametro1>,<parametro2>,..)]

O código ficaria da seguinte forma:

TCliente = class
  private
    ...
  public
    property Codigo : Integer read FCodigo write FCodigo;
    
    [TValidacao_NotNulll('Informe o nome!')]
    property Nome : String read FNome write FNome;

    [TValidacao_NotNulll('Informe o sobrenome!')]
    property SobreNome : String read FSobreNome write FSobreNome;

    [TValidacao_RangeValues(1,10, 'Idade inválida')]
    property Idade : Integer read FIdade write FIdade;

    ...
  end;

Os parâmetros utilizados na declaração do atributo são os mesmos do método construtor da classe, respeitando inclusive a ordem dos parâmetros definida.

O terceiro passo é consumir as informações dos atributos. O código a seguir implementa uma rotina de validação de objetos, de acordo com a definição dos atributos junto às classes.

function Exemplo11(pObjeto : TObject) : Boolean;
var
  ctxRtti  : TRttiContext;
  typeRtti : TRttiType;
  propRtti : TRttiProperty;
  atrbRtti : TCustomAttribute;

begin

  Result   := True;

  ctxRtti  := TRttiContext.Create;
  typeRtti := ctxRtti.GetType( pObjeto.ClassType );

  for propRtti in typeRtti.GetProperties do
    for atrbRtti in propRtti.GetAttributes do
      if atrbRtti Is TValidacao then
        if not (atrbRtti as TValidacao).Validar(propRtti.GetValue(pObjeto)) then
          Result := False;

  ctxRtti.Free;

end;

A rotina acima percorre todas as propriedades do objeto informado pelo parâmetro pObjeto. Para cada propriedade, são pesquisados os atributos e se eles descendem da classe TValidacao. Então a rotina Validar é invocada, informando o valor atual da propriedade daquele objeto.

Para realizar um teste, utilizaremos o código a seguir que instancia um objeto TCliente sem informar o SobreNome e com uma Idade inválida.

Ao executar o método Exemplo11 são exibidas duas caixas de mensagem com o conteúdo “Informe o sobrenome!” e “Idade inválida!”, de acordo com as restrições impostas pelos atributos.

procedure TForm4.btnValidacaoClick(Sender: TObject);
var
  newCliente : TCliente;

begin

  newCliente := TCliente.Create;
  newCliente.Codigo := 20;
  newCliente.Nome   := 'Rodrigo';
  newCliente.Idade  := 80;

  Exemplo11(newCliente);

  newCliente.Free;
end;

Embora este exemplo seja bastante minimalista, você pode observar como é possível implementar um mecanismo de validação flexível e prático. Já que praticamos descoberta dinâmica de tipos, utilizando RTTI, a rotina Exemplo11 poderia ser utilizada com qualquer outro tipo (TFornecedor, TVenda, etc.) sem sofrer nenhuma alteração.

    Utilidade de Atributos

Com um pouco de criatividade, você pode utilizar atributos em várias ocasiões para facilitar o desenvolvimento e aumentar a produtividade.

Uma destas ocasiões, bastante comentadas na internet, seria criar um mecanismo que realize todo o processo de iteração entre objetos de negócio e banco de dados, automaticamente. Uma das partes mais difíceis deste processo é realizar o mapeamento dos objetos, indicando de qual tabela e campo àquele campo corresponde. Tratamento de diferentes tipos de dados, tamanho de variáveis, etc...

Com atributos, tudo fica mais fácil. Só uma palhinha:

  [TTabela('CLIENTES')]
  TCliente = class
  private
    FCodigo: Integer;
    FNome: String;
    FIdade: Integer;
    FSobreNome: String;
  public

    [TCampoInteger('ID', ftInteger, poNotNull)]
    property Codigo : Integer read FCodigo write FCodigo;

    [TCampoString('FIRST_NAME', ftVarChar, 50, poNotNull)]
    property Nome : String read FNome write FNome;

    [TCampoString('LAST_NAME', ftVarChar, 50)]
    property SobreNome : String read FSobreNome write FSobreNome;

    [TCampoInteger('AGE', ftInteger)]
    property Idade : Integer read FIdade write FIdade;

  end;

Utilizando os conceitos apresentados neste artigo, você pode facilmente capturar os atributos que descrevem a tabela no banco de dados e escrever comandos SQL para instruções INSERT, DELETE, UPDATE e SELECT automaticamente. Reutilização de código total. Isso é OOP (Programação Orientação a Objetos)!

    Conclusão

Embora seja um assunto pouco abordado pela comunidade, a RTTI pode ser muito útil no seu dia-a-dia. Ela permite a você realizar algumas tarefas em menos tempo e viabiliza soluções que não seriam possíveis de outra forma.

Mesmo que o uso pareça um pouco obscuro inicialmente, procure implementar alguns exemplos e ouse pensar em novas formas de resolver velhos problemas. Em pouco tempo, você não conseguirá viver sem ela!

Server Response from: ETNASC03