Que título estranho para um artigo, não?
Vou explicar melhor essa antítese. Quando precisamos trabalhar com uma instância de uma classe, devemos referenciá-la na seção uses da unit, correto? Bom, mais ou menos. O objetivo deste artigo é apresentar uma forma de utilizar uma classe sem necessariamente adicionar a sua referência na unit. O nome dessa mágica é RTTI!
André, RTTI de novo?
Sim! Apenas para esclarecer, pessoal: o RTTI é um universo de possibilidades. É bem provável que ainda haverá outros artigos sobre este tópico aqui no blog. A ideia é explorar, aos poucos, a capacidade deste recurso para demonstrar as vantagens que podemos obter com a sua utilização.
A biblioteca de RTTI do Delphi disponibiliza um record chamado TRttiContext
, do namespace System.Rtti
. Por meio deste, ganhamos acesso às estruturas das classes que compõem o projeto. Para isso, utilizamos o método GetType
informando o tipo como parâmetro, exemplificado no código abaixo:
1 2 3 4 5 6 7 8 9 |
var RttiContext: TRttiContext; RttiType: TRttiType; begin RttiContext := TRttiContext.Create; RttiType := RttiContext.GetType(TMinhaClasse.ClassInfo); // Ações com o RttiType... end; |
Além do GetType
, o record TRttiContext
também fornece o método FindType
para encontrar um tipo existente pelo nome, porém, com uma ressalva: o parâmetro deve vir acompanhado do nome da unit. Veja o exemplo a seguir:
1 2 3 4 5 6 7 8 9 |
var RttiContext: TRttiContext; RttiInstanceType: TRttiInstanceType; begin RttiContext := TRttiContext.Create; RttiInstanceType := RttiContext.FindType('Unit1.TMinhaClasse').AsInstance; // Operações com RttiInstanceType... end; |
Exemplo
Vamos adiante. Para demonstrar essa funcionalidade, decidi abordar um exemplo prático.
Considere a simples unit a seguir, que declara uma classe com um único método para abrir o bloco de notas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
unit UnitMinhaClasse; interface type TMinhaClasse = class public procedure AbrirNotepad; end; implementation uses Windows, ShellAPI; { TMinhaClasse } procedure TMinhaClasse.AbrirNotepad; begin ShellExecute(0, 'open', 'Notepad.exe', nil, nil, SW_SHOWNORMAL); end; initialization TMinhaClasse.ClassName; end. |
Notou algo de incomum? A instrução no initialization
, não é?
Pois bem, como a classe não é referenciada em outras units, o linker do Delphi tende a ignorá-la na produção do executável. Neste caso, obviamente, o método FindType
do TRttiContext
não será capaz de encontrá-la, portanto, adicionei uma simples instrução na seção initialization
(um workaround) para que ela possa considerada pelo linker.
Agora, em outra unit, chamaremos o método dessa classe sem precisar referenciá-la na seção uses. Acredita ou não? 🙂
Para que isso seja possível, utilizaremos o GetMethod
(para encontrar o método pela descrição) e o Invoke
(para executar o método) das classes TRttiInstanceType
e TRttiMethod
, respectivamente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var RttiContext: TRttiContext; RttiInstanceType: TRttiInstanceType; RttiMethod: TRttiMethod; Instance: TValue; begin RttiContext := TRttiContext.Create; // Encontra o tipo RttiInstanceType := RttiContext.FindType('UnitMinhaClasse.TMinhaClasse').AsInstance; // Encontra o constructor (pelo nome "Create") RttiMethod := RttiInstanceType.GetMethod('Create'); // Invoca o construtor, atribundo a instância à variável "Instance" Instance := RttiMethod.Invoke(RttiInstanceType.MetaclassType,[]); // Encontra o método "AbrirNotepad" RttiMethod := RttiInstanceType.GetMethod('AbrirNotepad'); // Invoca o método RttiMethod.Invoke(Instance, []); end; |
Agora você pode acreditar! 😀
E se for necessário passar parâmetros?
Sem problemas! Na chamada do método Invoke
, observe a presença dos colchetes. Dentro dele, podemos informar os parâmetros do método que será executado. Vamos ver como funciona?
Em TMinhaClasse
, haverá um novo método, bem simples, para somar dois valores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type TMinhaClasse = class public { ... } function Somar(const Valor1, Valor2: integer): string; end; implementation { ... } function TMinhaClasse.Somar(const Valor1, Valor2: integer): string; begin result := 'Resultado: ' + IntToStr(Valor1 + Valor2); end; |
Em seguida, faremos o mesmo procedimento: executar o método da classe sem referenciar a unit.
O código é basicamente o mesmo apresentado acima, salvo que, dessa vez, informamos os parâmetros ao invocar o método. Como exemplo, indiquei os números 10 e 20 e optei por remover os comentários para facilitar a leitura:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var RttiContext: TRttiContext; RttiInstanceType: TRttiInstanceType; RttiMethod: TRttiMethod; Instance: TValue; begin RttiContext := TRttiContext.Create; RttiInstanceType := RttiContext.FindType('UnitMinhaClasse.TMinhaClasse').AsInstance; RttiMethod := RttiInstanceType.GetMethod('Create'); Instance := RttiMethod.Invoke(RttiInstanceType.MetaclassType,[]); RttiMethod := RttiInstanceType.GetMethod('Somar'); ShowMessage(RttiMethod.Invoke(Instance, [10, 20]).AsString); end; |
Impressionante, não?
Faça proveito deste recurso do RTTI para criar bibliotecas de chamadas ou para evitar dependências explícitas entre units. Além disso, é um ótimo aliado ao se trabalhar com Generics.
Fico por aqui, leitores! Volto em breve com o próximo artigo da série sobre Design Patterns!
Abração!
Andrézão, muito legal isso ai cara. RTTI sempre foi temido por nós Delpheiros, mas acredito que essa técnica tem muito a agregar quando se trabalha com frameworks, BPPS (Boas Práticas em Produção de Software) e DSS (Desenvolvimento Sustentável de Software). No momento estou muito focado em Reflection para tentar reproduzir um bom ORM para Delphi como Realm é para Android e Swift.
Opa, obrigado pelo comentário, Marcão!
Devo confessar que eu mesmo desconhecia a capacidade do RTTI até há alguns meses. Hoje compreendo perfeitamente a maneira como um ORM é criado no Delphi. Siga firme nessa ideia do ORM! A comunidade Delphi agradece. 🙂
A propósito, já assisti o vídeo sobre Clean Architecture. Vamos conversar em breve sobre isso!
Abraço!
Na verdade você cria a classe.
Quado você invoca esse cara -> RttiInstanceType.GetMethod(‘Create’), você esta invocando o construtor da classe.
Podemos fazer quase tudo com a RTTI nova no Delphi, rs.
Outra coisa na passagem de parâmetro via RTTI você deve passar um array de TValue, e caso o tipo da varíavel seja diferente do método ele explode um invalid type cast, se não me engano rsrs.
Uma dúvida, o initialization não esta errado?
Eu uso esse cara: Classes.RegisterClass(XPTO).
Mas parabéns pelo artigo =]
Grande Oliveira! Mestre do RTTI! 🙂
Exatamente, são 2 passos: invocar o construtor para obter uma instância da classe e depois executar o método desejado.
O TMinhaClasse.ClassName no initialization é apenas uma instrução simbólica para que o linker inclua a classe no executável. A classe não precisa estar registrada com o RegisterClass, ao menos que alguma unit do projeto use o método GetClass para encontrá-la, mas não é o caso do FindType do RTTI.
Obrigado, Oiveira!
Interessante… é um tipo de magia negra…
Parece magia mesmo, Clayton, rsrs.
Sensacional, André!
Mais uma vez os seus artigos – que são muito bem escritos, de maneira clara e com ótimos exemplos – seguem abrindo a mente dos desenvolvedores Delphi. Confesso que na prática ainda não faço uso do RTTI, o que conheço é apenas no campo da teoria. Mas após ler isso, confesso que estou bastante motivado a adotar o uso nos projetos aqui da empresa, pois o baixo acoplamento entre classes é uma batalha diária para quem procura programar seguindo as boas práticas.
Forte abraço!
Muito obrigado, Cleo!
As bibliotecas de RTTI passaram por uma grande “transformação” nas versões mais recentes do RAD Studio. Meu objetivo é apresentar um pouco desse novo “paradigma” sempre que possível! 🙂
Também compartilho dessa batalha para conquistar uma boa arquitetura. Acredito que, só pelo fato de termos essa preocupação, já estamos no caminho certo!
Grande abraço!
Caro Andre.
Somos uma empresa de desenvolvimento de produtos para controle de acesso e ponto. Estamos procurando empresas ou profissionais para contratar o desenvolvimento de um aplicativo mobile. Como desenvolvemos nossos produtos em Delphi, o ideal seria mantermos nesta plataforma também para o desenvolvimento mobile.
Acessei seu site via site da Embarcadero (MVPs).
Caso esteja interessado em desenvolver nosso app, por favor entre em contato.
Obrigado.
Olá, Cláudio, tudo bem?
Entrarei em contato para solicitar mais detalhes!
Obrigado pelo contato.
Uma dúvida, André.
E no final teria que dar FreeAndNil(RttiContext)? Ou é liberado dinamicamente da memória?
Bom dia, Oteniel!
Como o
TRttiContext
é um record, não é necessário liberá-lo. O próprio ARC (Automatic Reference Counting) do Delphi já se encarrega desse trabalho.Abraço!
Boa tarde André, muito bom a sua explicação, eu estou utilizando o que foi exemplificado aqui para criar um utilitário de linha de comando para rodar alguns processos separadamente, porém está me reportando memory leak, estou tentando identificar algum objeto para destruir mas não estou encontrando rsrsrs.
Boa noite, Francis, tudo bem?
No código de exemplo deste artigo, a variável “Instance” deve ser liberada da memória. Vou corrigir essa falha que inclusive já foi reportada por outros leitores. Verifique se este não é o seu caso também.
Abraço!
Opa, boa noite. Fiz um cast do retorno para o TValue me retornar um AsObject ficando mais simples a liberação… vlw… parabéns pelos artigos.
Ótima solução, Francis!
Obrigado pela contribuição!
Abraço!
Parabéns pela publicação.
Estou tentando executar seu exemplo e como não conhecia RTTI, estou com dificuldade. Seguinte, tenho um form main e crio form filhos a partir desse. Dentro do main resolvi criar uma procedure que chama outros formulários passando parâmetros para cada um, então fiz 2 parâmetros: um indica o form a chamar (0,1,2,3 etc) e outro é um array de string, porém quando tento passar os parâmetros na compilação diz que o invoke não tem versão com parâmetros e não pode ser chamado com argumentos. Se chama sem argumentos a procedure reclama que não tem argumentos.
Agradeço, forte abraço.
Olá, Carlos.
Vou entrar em contato com você para pedir mais detalhes, ok?
Abraço!
Muito interessante, mas com essa abordagem encontrei problema com Generics… no meu caso estou fazendo:
Porém, de alguma a classe consegue ser encontrada… qual o mistério ?
Nem assim : RttiContext.FindType(‘TServiceCliente’).AsInstance;
Nem assim : RttiContext.FindType(‘Unit1.TServiceCliente’).AsInstance;
Olá, Lindemberg.
Acho que o WordPress removeu os sinais de maior e menor para representar Generics.
Vou entrar em contato com o e-mail para que você possa enviar o código completo.
Abraço!
Muito bom André, parabéns.
Uma dúvida apenas. Eu conseguiria acessar os métodos de um class helper usando RTTI?
Exemplo:
Tenho vários enums, e todos os enums tem o Helper que me retorna o GetValue dele que é salvo no banco de dados (no banco não salva o índice do enum… banco é velho e não começou assim), e tambem tenho um método ToString que retorna a descrição do enum para o usuário.
Queria ter um método que possa receber qualquer um desses enums, e acessar os métodos GetValue e ToString. Tem como fazer isso com RTTI?
Olá, Vinícius! Desculpe-me pela demora!
Infelizmente não é possível acessar os métodos de um helper via RTTI. A estrutura de um class helper não fica visível no contexto.
De qualquer forma, você está no caminho certo. Se a ideia é, por exemplo, receber o valor “NAO” do enumerado e chamar o método
ToString
para retornar o texto “Não”, o record helper há funciona!Veja esse exemplo do método
ToString
:E então um exemplo de uso:
Abraço!
Obrigado Andre,
Era isso que não queria ouvir: “Infelizmente não é possível acessar os métodos de um class helper via RTTI”, kkk, mas muito obrigado pela ajuda… e mais uma vez parabéns pelos seus artigos.
Tranquilo, Vinícius! 🙂
Te aviso se houver qualquer novidade sobre este assunto.
Abraço!
Modifiquei um pouco o código para meu WebService RestDataware, assim eu não preciso ficar criando as classes de eventos.
Espero que ajude alguém 😀
Olá, Gabriel!
Agradeço pela sua contribuição! Como muita gente usa o RDW, esse código será bastante útil.
Obrigado! Abraço!