A Reflexão (Reflection) é uma funcionalidade poderosa do framwork .NET que permite a um programa inspecionar e manipular informações de tipos em tempo de execução. Através da reflexão, é possível criar objetos dinamicamente, invocar métodos, e acessar propriedades e campos de forma programática.
A reflexão fornece objetos que encapsulam assemblies, módulos e tipos (geralmente do tipo Type). Ela possibilita a criação dinâmica de instâncias de um tipo, vincular um tipo a um objeto existente, ou obter o tipo de um objeto para então invocar seus métodos ou acessar seus campos e propriedades. Se o código utilizar attributes, a reflexão também permite acessá-los.
Distinção entre Arquivos EXE e DLL
No sistema operacional Windows, os formatos EXE e DLL possuem propósitos e características distintas.
- EXE (Executável): Um arquivo de aplicação endependente que pode ser iniciado diretamente pelo sistema operacional.
- DLL (Biblioteca de Vínculo Dinâmico): Contém código e dados compartilháveis por múltiplos programas, sendo carregada em tempo de execução.
A principal diferença estrutural reside no ponto de entrada: um EXE contém um método Main, enquanto uma DLL não. Em termos de desempenho, DLLs podem ser compartilhadas na memória entre processos, economizando recursos.
Metadados em C# e .NET
Os metadados são informações armazenadas em um assembly (arquivo EXE ou DLL) que descrevem seus tipos, membros, atributos e dependências. Eles são fundamentais para funcionalidades como a reflexão, serialização, debugging e para o funcionamento de ferramentas de desenvolvimento.
Criando uma DLL
Para criar uma DLL em um ambiente de desenvolvimento como o Visual Studio, o processo geral é:
- Criar uma nova solução e adicionar um projeto do tipo Class Library.
- Desenvolver as classes, interfaces e outros tipos necessários dentro deste projeto.
- Compilar o projeto (Build ou Rebuild). O compilador gerará um arquivo
.dllna pastabin\Debugoubin\Releasedo projeto.
Principais Usos da Reflexão
- Carregamento Dinâmico de Assemblies: Usar a classe
Assemblypara carregar bibliotecas e extrair informações de seus tipos. - Criação e Manipulação de Objetos: Utilizar
Activator.CreateInstancepara instanciar objetos com base em informações de tipo obtidas em runtime. - Acesso a Membros Privados: Embora desencorajado, é possível acessar campos e métodos privados de uma classe.
- Introspecção de Tipo: Obter informações sobre as propriedades, campos, métodos e construtores de uma classe.
- Invocação Dinâmica de Métodos: Chamar métodos sem conhecê-los em tempo de compilação.
- Leitura e Escrita de Propriedades: Acessar dinamicamente os valores das propriedades de um objeto.
Exemplos Práticos de Reflexão
Carregando um Assembly
Existem diferentes métodos para carregar um assembly, cada um com seu caso de uso:
Assembly.Load
Carrega um assembly pelo seu nome, procurando no diretório base do aplicativo ou no GAC (Global Assembly Cache).
Assembly meuAssembly = Assembly.Load("MinhaBiblioteca");
Assembly.LoadFrom
Carrega um assembly a partir de um caminho de arquivo específico (absoluto ou relativo).
Assembly meuAssembly = Assembly.LoadFrom(@"C:\Projetos\libs\MinhaBiblioteca.dll");
Assembly.LoadFile
Carrega um assemb com base em um caminho de arquivo, sem resolver automaticamente suas dependências.
Assembly meuAssembly = Assembly.LoadFile(@"C:\Projetos\libs\MinhaBiblioteca.dll");
Carregando a partir de um Byte Array
Este método permite carregar um assembly diretamente de uma matriz de bytes, útil em cenários como o carregamento de assemblies compilados em memória.
byte[] bytesDoAssembly = File.ReadAllBytes(@"C:\Projetos\libs\MinhaBiblioteca.dll");
Assembly meuAssembly = AppDomain.CurrentDomain.Load(bytesDoAssembly);
Enumerando Tipos em um Assembly
Após carregar um assembly, podemos iterar sobre todos os tipos (classes, structs, etc.) definidos nele.
byte[] bytesDoAssembly = File.ReadAllBytes(@"C:\Projetos\libs\MinhaBiblioteca.dll");
Assembly assemblyCarregado = AppDomain.CurrentDomain.Load(bytesDoAssembly);
foreach (Type tipo in assemblyCarregado.GetTypes())
{
Console.WriteLine(tipo.Name);
}
// Saída exemplo:
// Funcionario
// Gerente
// RepositorioGenerico`1
Criando uma Instância de um Objeto
Construtor sem Parâmetros
Considere a classe abaixo, que possui um construtor padrão sem parâmetros.
public class Funcionario
{
public string NomeCompleto { get; set; }
public int Idade { get; set; }
public Funcionario()
{
NomeCompleto = "Não definido";
Idade = 0;
}
public void ExibirDados()
{
Console.WriteLine($"Nome: {NomeCompleto}, Idade: {Idade}");
}
}
Criando uma instância usando reflexão:
Type tipoFuncionario = typeof(Funcionario);
object instanciaFuncionario = Activator.CreateInstance(tipoFuncionario);
Construtor com Parâmetros
Se a classe definir apenas construtores com parâmetros, o Activator.CreateInstance precisa receber esses argumentos.
public class Funcionario
{
public string NomeCompleto { get; set; }
public int Idade { get; set; }
public Funcionario(string nome, int idade)
{
NomeCompleto = nome;
Idade = idade;
}
public void ExibirDados()
{
Console.WriteLine($"Nome: {NomeCompleto}, Idade: {Idade}");
}
}
// ...
Type tipoFunc = typeof(Funcionario);
object instFunc = Activator.CreateInstance(tipoFunc, "Maria Silva", 28);
Manipulando Propriedades via Reflexão
Podemos ler e escrever valores de propriedades de forma dinâmica.
// Partindo de uma instância já criada (instFunc)
Type tipo = instFunc.GetType();
// Alterando o valor da propriedade 'NomeCompleto'
PropertyInfo propNome = tipo.GetProperty("NomeCompleto");
propNome.SetValue(instFunc, "Ana Pereira");
// Lendo o valor da propriedade
string nomeAtual = (string)propNome.GetValue(instFunc);
Console.WriteLine(nomeAtual); // Saída: Ana Pereira
// Alternativa usando dynamic
Console.WriteLine(((dynamic)instFunc).NomeCompleto);
Introspecção e Invocação de Métodos
Listando Métodos e Parâmetros
Type tipoFunc = typeof(Funcionario);
foreach (MethodInfo metodo in tipoFunc.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
Console.WriteLine($"Método: {metodo.Name}");
foreach (ParameterInfo parametro in metodo.GetParameters())
{
Console.WriteLine($" - Parâmetro: {parametro.Name} (Tipo: {parametro.ParameterType.Name})");
}
}
// Saída exemplo:
// Método: ExibirDados
// - (Sem parâmetros para este método específico)
Invocando um Método de Instância
// Assumindo que 'instFunc' já foi criado
MethodInfo metodoExibir = tipoFunc.GetMethod("ExibirDados");
metodoExibir.Invoke(instFunc, null); // Chama instFunc.ExibirDados()
Invocando um Método Estático
Para métodos estáticos, passamos null como instância.
public class Calculadora
{
public static int Somar(int a, int b) => a + b;
}
// ...
Type tipoCalc = typeof(Calculadora);
MethodInfo metodoSomar = tipoCalc.GetMethod("Somar");
object[] argumentos = new object[] { 10, 5 };
int resultado = (int)metodoSomar.Invoke(null, argumentos); // resultado = 15
Trabalhando com Tipos Genéricos
Criando uma Instância de uma Classe Genérica
É necessário primeiro construir o tipo genérico concreto.
public class Repositorio<t>
{
public T Entidade { get; set; }
public Repositorio(T entidade)
{
Entidade = entidade;
}
}
// ...
Type tipoGenericoAberto = typeof(Repositorio<>);
Type tipoGenericoConcreto = tipoGenericoAberto.MakeGenericType(typeof(Funcionario));
object repositorio = Activator.CreateInstance(tipoGenericoConcreto, instFunc); // instFunc é uma instância de Funcionario</t>
Invocando um Método Genérico
Para métodos genéricos, precisamos especificar os argumentos de tipo.
public class Utilidades
{
public static void ExibirTipos<t1 t2="">(T1 valor1, T2 valor2)
{
Console.WriteLine(valor1);
Console.WriteLine(valor2);
}
}
// ...
Type tipoUtil = typeof(Utilidades);
MethodInfo metodoGenerico = tipoUtil.GetMethod("ExibirTipos");
MethodInfo metodoGenericoConcreto = metodoGenerico.MakeGenericMethod(typeof(string), typeof(int));
metodoGenericoConcreto.Invoke(null, new object[] { "Olá", 123 });</t1>
Considerações Importantes
- Desempenho: A reflexão é significativamente mais lenta que o acesso direto a tipos. Seu uso deve ser ponderado, especialmente em código crítico para performance.
- Encapsulamento: Acessar membros privados via reflexão quebra o encapsulamento do objeto, devendo ser evitado a menos que seja estritamente necessário.
- Segurança: A reflexão pode contornar as verificações de tipo feitas em tempo de compilação, introduzindo potenciais riscos de segurança.