A reflection permite que um programa examine tipos enquanto executa: quais propriedades uma classe tem, quais são os valores delas, quais métodos existem, quais atributos estão ligados. Os atributos são a outra metade: marcações declarativas entre colchetes, como [Obsolete] ou [JsonPropertyName("id")], que não significam nada sozinhas até que o compilador ou algum código as leia com reflection. Serializadores, ORMs, bibliotecas de validação, frameworks de teste e o roteamento do ASP.NET funcionam todos assim.
A maior parte do código de aplicação raramente precisa de reflection diretamente, mas saber como ela funciona explica muito do comportamento dos frameworks.
Objetos Type: typeof e GetType
Tudo começa em um System.Type. Há duas formas de obter um:
Saída:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
A diferença para lembrar: typeof(X) nomeia um tipo que você conhece ao escrever o código, e obj.GetType() pergunta a um objeto o que ele realmente é. Comparar GetType() == typeof(Employee) é uma correspondência exata que falha para classes derivadas, o que normalmente não é o que você quer; is e IsAssignableFrom respeitam a herança. nameof parece parecido, mas não é reflection: o compilador o substitui por uma constante string.
GetType() em uma referência null lança NullReferenceException, já que não há objeto para perguntar. Em um tipo de valor anulável com boxing, ele retorna o tipo subjacente: ((int?)5).GetType() é System.Int32.
Lendo e definindo propriedades
GetProperties() lista as propriedades públicas de um tipo como objetos PropertyInfo, e cada um deles consegue ler e escrever o valor em uma instância:
Saída:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Três coisas que isso mostra:
GetValueretornaobject, então os tipos de valor voltam com boxing e você precisa de um cast para usá-los.GetPropertycom um nome que não existe retornanull, e a próxima chamada nele lançaNullReferenceException. Verifique antes de usar.BindingFlags.NonPublic | BindingFlags.Instancealcança membros privados. Isso é legítimo em ferramentas e testes, mas contorna o encapsulamento e quebra sem aviso quando a classe é refatorada.
Esse laço é basicamente como funciona um exportador de CSV ou um serializador de objeto para JSON: percorrer as propriedades, ler cada valor, formatá-lo.
Chamando métodos e criando objetos pelo nome
GetMethod encontra um método, e Invoke o chama com um array de argumentos. Activator.CreateInstance cria um objeto a partir de um Type, que é como sistemas de plugins e contêineres de injeção de dependência constroem tipos escolhidos em tempo de execução:
Saída:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly limita a lista aos membros declarados na própria classe; sem ele, GetMethods também retorna ToString, Equals, GetHashCode e GetType de object. Type.GetType("Name") precisa do nome qualificado com o namespace e, para tipos em outros assemblies, também do nome do assembly ("MyApp.Plugins.Csv, MyApp.Plugins").
Se o método invocado lançar uma exceção, Invoke a envolve em uma TargetInvocationException; a original fica na InnerException dela.
Atributos: marcações que o compilador e os frameworks leem
Um atributo é escrito entre colchetes antes daquilo que ele descreve. O framework define muitos; alguns que mudam o que o compilador faz:
public class OrderService
{
[Obsolete("Use PlaceOrderAsync instead.")]
public void PlaceOrder(Order order) { }
// Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
// [Obsolete("...", true)] makes it error CS0619 instead.
[Conditional("DEBUG")]
public void Trace(string message) => Console.WriteLine(message);
// Calls to Trace are removed entirely from builds without the DEBUG symbol.
}
[Flags] enum Channels { None = 0, Email = 1, Sms = 2 } // changes how ToString formats combinations
[Serializable] class Snapshot { } // marks a type for legacy binary serialization
Outros são lidos por bibliotecas em tempo de execução: [JsonPropertyName] e [JsonIgnore] pelo System.Text.Json, [Required] e [MaxLength] pela validação de modelos do ASP.NET Core e pelo Entity Framework, [HttpGet("orders/{id}")] pelo roteamento do ASP.NET, [Fact] e [Test] pelos executores de testes. O atributo em si não faz nada; quem faz é o código que o procura.
O nome ObsoleteAttribute é encurtado para [Obsolete] ao ser aplicado: por convenção, o nome de toda classe de atributo termina em Attribute, e C# deixa você omitir o sufixo.
Declarando e lendo um atributo personalizado
Um atributo personalizado é uma classe que deriva de Attribute. [AttributeUsage] diz a que ele pode ser aplicado. Os parâmetros do construtor viram argumentos posicionais, e as propriedades públicas com setter viram argumentos nomeados:
Saída:
Username must be at most 20 characters
Email is required
Keep the city code short
0
É uma versão em miniatura do que a validação de modelos do ASP.NET Core faz com System.ComponentModel.DataAnnotations. Os argumentos de atributos precisam ser constantes de tempo de compilação (números, strings, typeof(...), valores de enum, ou arrays deles), porque são guardados nos metadados do assembly. GetCustomAttribute<T>() é um método de extensão em System.Reflection; também existe IsDefined(typeof(T)) para quando você só precisa saber se um atributo está presente.
O custo da reflection
A reflection troca velocidade e segurança por flexibilidade:
- Velocidade. Buscar um membro pelo nome e chamá-lo por meio de
InvokeouGetValueé muito mais lento que uma chamada direta, e faz boxing dos tipos de valor. Para uso repetido, busque oPropertyInfoou oMethodInfouma vez e guarde-o, ou transforme-o em um delegate comDelegate.CreateDelegateouMethodInfo.CreateDelegatee chame esse delegate. - Segurança. Um nome escrito errado ou uma assinatura alterada compila sem problemas e falha em tempo de execução. Prefira
nameof(Product.Price)à string"Price"sempre que puder, para que as renomeações sejam pegas. - Trimming e AOT. Apps com trimming e Native AOT removem os membros que parecem não ser usados, e a reflection esconde esse uso da análise. Bibliotecas modernas (
System.Text.Json,GeneratedRegex, logging) estão migrando para source generators, que fazem o mesmo trabalho em tempo de compilação.
Use reflection nas partes de um programa que de fato não conhecem seus tipos de antemão: plugins, ferramentas genéricas, serializadores, auxiliares de testes. Quando os tipos são conhecidos, código comum, genéricos ou interfaces são mais rápidos e verificados pelo compilador.
Erros comuns
GetType() == typeof(Base)para testar uma classe base. Falha para os tipos derivados. UseisouIsAssignableFrom.- Não verificar
null.GetProperty,GetMethodeType.GetTyperetornamnullquando nada corresponde. - Reflection em um laço crítico sem cache. Guarde o
MemberInfoem cache ou compile um delegate. - Capturar a exceção errada de
Invoke. A exceção real é aInnerExceptiondaTargetInvocationException. - Strings mágicas para nomes de membros. Use
nameof.
Perguntas frequentes
O que é reflection em C#?
Reflection é a capacidade de um programa inspecionar tipos em tempo de execução: listar as propriedades e os métodos de uma classe, ler e definir valores pelo nome, chamar métodos, criar instâncias e ler atributos. Ela fica em System.Reflection e começa a partir de um objeto Type. Serializadores, ORMs, contêineres de injeção de dependência e frameworks de teste são construídos sobre ela.
Qual a diferença entre typeof e GetType em C#?
typeof(Customer) é resolvido em tempo de compilação a partir de um nome de tipo e não precisa de objeto. obj.GetType() é chamado em uma instância em tempo de execução e retorna o tipo real do objeto, que pode ser mais derivado que o tipo declarado da variável: para Animal a = new Dog();, a.GetType() é Dog. GetType() em uma referência null lança NullReferenceException.
Como obter o valor de uma propriedade pelo nome em C#?
obj.GetType().GetProperty("Price") retorna um PropertyInfo (ou null se não existir essa propriedade pública), e .GetValue(obj) a lê como object. .SetValue(obj, value) a escreve. Guarde o PropertyInfo em cache se fizer isso em um laço, porque a busca é a parte cara.
Como criar um atributo personalizado em C#?
Declare uma classe que deriva de System.Attribute, dê a ela um nome com o sufixo Attribute e marque onde ela pode ser usada com [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Aplique-o como [MaxLength(50)] e leia-o com property.GetCustomAttribute<MaxLengthAttribute>().
O que o atributo Obsolete faz em C#?
[Obsolete("Use PlaceOrderAsync instead")] em um membro faz o compilador emitir o aviso CS0618, com a sua mensagem, em cada ponto de chamada. [Obsolete("...", true)] transforma o aviso no erro CS0619. É assim que bibliotecas aposentam uma API sem quebrar quem a chama da noite para o dia.
Reflection é lenta em C#?
Comparada com uma chamada direta, sim: encontrar um membro pelo nome e invocá-lo por meio de MethodInfo.Invoke ou PropertyInfo.GetValue costuma ser de dezenas a centenas de vezes mais lento, e faz boxing dos tipos de valor. Ela serve para inicialização, configuração e uso ocasional. Em caminhos críticos, guarde o MemberInfo em cache, monte um delegate uma vez, ou use genéricos ou um source generator.