O artigo começa a expor as ferramentas existentes no framework .NET para a implementação das técnicas de Reflection que na programação cuidam de descobrir dados sobre classes e métodos de outros programas e bibliotecas. Isto é feito demonstrando uma aplicação que preenche dados de qualquer classe com dados vindos de bancos de dados. O artigo mostra também como desenvolver programas usando os recursos de modularização separando cada tarefa em umalis biblioteca diferente e permitindo o isolamento de tarefas com os user controls.
Para que serve
As ferramentas existentes na biblioteca System.Reflection permitem ao desenvolvedor conhecer os métodos, propriedades, construtores, parâmetros e campos existente nas classes dos programas escritos em .NET. Com isso, podem ser criados códigos que tenham mais flexibilidade se adaptando a diversas situações.
Em que situação o tema é útil
Para o desenvolvimento de extensões de softwares, plug-ins ou add-ins. Com este tipo de aplicação novas funcionalidades são acrescentadas para softwares existentes aumentando sua utilidade. Também pode se usar Reflection para criar bibliotecas para acessar bancos de dados que se conectem com diversos tipos de fontes de dados. Além disto, o mapeamento objeto-relacional que sempre foi um problema para o desenvolvimento de programas que usam banco de dados, ganha no uso de Reflection um grande aliado e facilitador. No projeto de exemplo, também será demonstrada uma maneira de obter metadados sobre registros consultados em um banco.
Reflection e Banco de Dados
Uma das principais preocupações quando se está desenvolvendo um programa é escrever um código que precise ser alterado o menor número de vezes possível e que tenha um alto grau de reutilização. Além disto, o desenvolvimento de aplicações que fazem uso de bancos de dados tende a perder um pouco das características de orientação a objetos pelo fato de bancos com estas características ainda não serem tão populares. Com Reflection é dado um passo adiante para que se possa obter mais flexibilidade no código e assim, diminuir o retrabalho. Considere por exemplo adicionar uma nova entidade em seu projeto que representa dados armazenados em um banco. No exemplo que é apresentado no artigo, uma aplicação Windows demonstra como usar reflection para diretamente preencher os dados da classe lendo de tabelas do banco sem fazer uso dos geradores de código do Visual Studio.
Com o surgimento da programação orientada a objetos muitas tarefas repetitivas diminuíram porque conceitos como reutilização, polimorfismo e encapsulamento começaram a fazer parte do vocabulário e das ferramentas que o programador tem acesso. No centro estão as classes que servem de modelos para objetos lógicos em torno dos quais os programas são desenvolvidos.
Seus campos, propriedades, métodos, construtores e parâmetros provêm todos os recursos para o desenvolvimento das funcionalidades contidas em um programa. O caminho natural para o desenvolvimento de software é que com o tempo, bibliotecas criadas anteriormente tornem-se mais reaproveitadas por já resolverem um determinado problema evitando o retrabalho.
Tudo o que o programador precisa é conhecer a estrutura interna destas bibliotecas: suas classes e componentes e passar a usá-las conforme indicado na sua documentação. Ops! Que documentação? Como fazer para utilizar estas bibliotecas quando não há documentação? E mais: é possível descobrir dados de uma classe durante o tempo de execução de um programa?
Neste ponto surge uma dúvida: por que seria interessante descobrir dados de um objeto durante o runtime da aplicação e não fazer isto no projeto prevendo todas as situações possíveis?
Porque assim o software ficaria bastante limitado. Em muitas situações isto é o melhor a fazer, mas, se a necessidade for criar um programa mais flexível e adaptável, onde novas funcionalidades podem ser acrescentadas com um mínimo esforço, a possibilidade de o código conhecer a estrutura interna dos objetos é importante e auxilia nesta tarefa. São estes tipos de informação – os dados internos dos objetos – que podem ser chamados de metadados.
Reflection
Reflection é o processo que permite ler os metadados de um programa que pode ser o que está sendo executado ou outro programa ou class library. Também permite inspecionar arquivos executáveis portáveis (arquivos executáveis e class library do framework .NET, também chamados de assembly ou pela sigla PE de Portable Executable) e preencher componentes de tipos e seus membros em tempo de execução.
O framework .NET define tipos que permitem analisar os metadados dos assemblys. Os que compõem System.Reflection são:
· Assembly;
· Module;
· ParameterInfo;
· MemberInfo;
· MethodInfo;
· ConstructorInfo;
· FieldInfo;
· EventInfo;
· PropertyInfo.
Os próximos estão em System.Type:
· Enum;
· Type.
A principal classe a ser usada nas tarefas de reflection é System.Type. É com ela que se descobre o nome tipo do dado, quais tipos estão presentes em um módulo (um assembly pode conter um ou vários módulos), se determinado tipo de dado é por valor ou referência. Também permite descobrir os métodos, propriedades e campos e eventos. As tarefas de serialização de dados usam reflection.
Por serialização entende-se como o processo onde um objeto é convertido em um array de bytes para que possa ser armazenado ou transmitido por qualquer meio que seja. Para que isto seja possível, os dados serializados precisam guardar também informações sobre sua estrutura para que o objeto possa ser reconstruído ao ser recuperado ou chegar ao seu destino. Os dois principais tipos de serialização são a binária e a XML onde um objeto é convertido para o formato XML usando sua estrutura de TAGS.
Através de reflection pode-se usar uma técnica de programação chamada de late binding. Esta técnica consiste em só carregar os módulos de um assembly a partir de parâmetros que só serão conhecidos em tempo de execução da aplicação, ou seja, ao criar o projeto não se conhece qual assembly (um módulo de uma dll por exemplo) deverá ser usado para executar determinada tarefa, então, para isto, os métodos contidos em System.Reflection.Assembly: Load, LoadFrom e LoadWithPartialName permitem este tipo de tarefa.
Antes de começar a trabalhar com reflection é importante conhecer um pouco sobre os assemblys e metadados dos arquivos executáveis do framework .NET.
Assembly e Metadata
Assembly: arquivo do tipo DLL ou EXE;
Manifest: descrição detalhada (metadados) de um assembly.
Um arquivo .exe ou .dll do framework .NET é principalmente formado por metadados e IL (Intermediate Language - a linguagem intermediaria entre o framework e o código nativo da máquina, somente interpretada pelo framework – Veja nota do DevMan).
Assim, um executável do .NET pode facilmente ser lido por outro, através do uso de reflection e ter os seus membros examinados e conhecidos durante o runtime. Os metadados contêm basicamente tabelas: de métodos, campos, propriedades, tipos, etc. É através destas tabelas que o System.Reflection pode obter dados sobre o assembly.
Ao ler os metadados todas as informações podem ser obtidas: nomes de propriedades, campos, métodos, construtores, atributos, tipos dos dados de cada um destes elementos, parâmetros que devem ser enviados para os métodos e construtores.
Também é possível descobrir os elementos pelo seu modo de acesso obtendo informações somente de elementos que são públicos, privados, de instância ou estáticos (static). Esta maneira como o .NET armazena os nomes dos elementos de um assembly é bastante útil para as tarefas relacionadas com reflection, mas, possuem a desvantagem de deixarem expostos os elementos. Podem existir casos em que isto não seja desejável para, por exemplo, proteger a propriedade intelectual de uma determinada empresa, ou, evitar cópias do código, muito embora, mesmo sendo possível escrever um programa usando IL, não é muito prático.
Para proteger um pouco mais o conteúdo do assembly é possível usar uma ferramenta que embaralha os nomes dos parâmetros usados no código, entre outras tarefas, o que dificulta um pouco a sua cópia. Mesmo que não seja grande coisa, já é uma forma a mais – existem outros utilitários para embaralhar o conteúdo do IL – de proteger o código que é gerado.
IL
A IL – Intermediate Language (também conhecida comoCommon Intermediate Language (CIL), ou aindaMicrosoft Intermediate Language (MSIL)) é conceito central do .NET Framework. Independente da linguagem de programação que você utilize, quando compila, seu código-fonte é convertido para IL. Entendendo IL você conseguirá: Entender melhor como o código gerenciado que você escreve realmente funciona; Examinar o código gerado pelo compilador (independente da linguagem); Escrever algum código diretamente em IL.
Em sua forma mais comum, Intermediate Language é uma linguagem binária. Como ocorre com a linguagem assembly nativa, uma instrução IL é armazenada em um container assembly (geralmente arquivos .exe ou .dll) em uma representação numérica binária.
Entretanto, como também ocorre com um código executável nativo, uma linguagem textual foi definida para representar a IL. Essa linguagem é constituída por códigos mnemônicos que, por sua vez, representam os comandos IL binários. Essa linguagem é conhecida como IL assembly. Entretanto, como IL Assembly é um nome muito longo, existe uma convenção em encurtar esse nome para ILAsm ou, simplesmente, código-fonte IL.
Todo arquivo executável (EXE) ou biblioteca (DLL) que seu compilador gera para você contém um código executável. Em .NET, esse código está escrito em uma especificação conhecida como Intermediate Language, que é binária. Além disso, existe uma forma textual de representar esse conteúdo executável.
Você pode ver a representação IL de seu código quando quiser. A Microsoft disponibiliza um utilitário que acompanha o SDK do .NET Framework, chamado ILDasm. Ao executá-lo, através do Command Prompt do Visual Studio, é possível escolher um arquivo binário, um programa qualquer feito em .NET. Além de exibir a estrutura do programa, o ILDasm ainda permite que se navegue nos métodos do mesmo. Basta dar dois cliques em um método para que o código IL correspondente seja apresentado.
A IL funciona sob o conceito de máquina virtual. Em palavras simples, a linguagem é baseada em uma arquitetura de computador imaginária (não fiel a como as coisas de fato acontecem no computador). A máquina virtual do .NET é conhecida como CLR (Common Language Runtime).
A CLR do .NET foi projetada para garantir consistência de tipos (type-safe) e isso é um dos fatores que torna o JIT tão eficiente na hora de converter o código IL para código nativo da máquina.
NA CLR temos, em alto-nível, variáveis locais, atributos de instância, atributos estáticos, variáveis globais, além de dados que foram passados como argumentos para os métodos. Embora o hábito seja pensar nesses dados de maneiras diferentes, como espécies de dados diferentes de memória, estas formas de dados são tratadas como da mesma espécie em posições diferentes de memória.
Tarefas básicas de Reflection
A primeira e mais elementar tarefa que pode ser executada é ler informações sobre um módulo de um programa, o código a seguir, que requer System.Reflection serve para obter todas as informações sobre o assembly atual:
Assembly theAssembly = Assembly.GetExecutingAssembly();
Sendo que a partir desta instância criada várias informações podem ser obtidas. Observe o exemplo a seguir:
void Main()
{
Assembly theAssembly = Assembly.GetExecutingAssembly();
Console.WriteLine( "Full name: {0}", theAssembly.FullName );
Console.WriteLine( "Location: {0}", theAssembly.Location );
}
Na primeira linha é criada a referência para o objeto. Em seguida o nome completo do programa, incluindo a versão e informações sobre a cultura são mostradas na tela. A propriedade Location mostra o caminho completo do arquivo onde o assembly está incluindo o nome do arquivo e pasta. Com este código é possível, por exemplo, descobrir a pasta onde o executável está localizado sendo que existem muitos outros elementos úteis que permitem.
...