La reflection permette a un programma di esaminare i tipi mentre è in esecuzione: quali proprietà ha una classe, quali sono i loro valori, quali metodi esistono, quali attributi sono associati. Gli attributi sono l'altra metà: etichette dichiarative tra parentesi quadre, come [Obsolete] o [JsonPropertyName("id")], che da sole non significano nulla finché il compilatore o del codice non le legge con la reflection. Serializzatori, ORM, librerie di validazione, framework di test e il routing di ASP.NET funzionano tutti così.
La maggior parte del codice applicativo raramente ha bisogno della reflection in modo diretto, ma sapere come funziona spiega molti comportamenti dei framework.
Oggetti Type: typeof e GetType
Tutto parte da un System.Type. Ci sono due modi per ottenerne uno:
Output:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
La differenza da ricordare: typeof(X) nomina un tipo che conosci mentre scrivi il codice, e obj.GetType() chiede a un oggetto che cosa sia davvero. Il confronto GetType() == typeof(Employee) è una corrispondenza esatta che fallisce per le classi derivate, cosa che di solito non vuoi; is e IsAssignableFrom rispettano l'ereditarietà. nameof sembra simile ma non è affatto reflection: il compilatore lo sostituisce con una costante stringa.
GetType() su un riferimento null lancia NullReferenceException, dato che non c'è alcun oggetto a cui chiedere. Su un tipo valore nullable in un box restituisce il tipo sottostante: ((int?)5).GetType() è System.Int32.
Leggere e impostare proprietà
GetProperties() elenca le proprietà pubbliche di un tipo come oggetti PropertyInfo, ognuno dei quali può leggere e scrivere il valore su una data istanza:
Output:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Tre cose che questo esempio mostra:
GetValuerestituisceobject, quindi i tipi valore tornano in un box e per usarli devi fare un cast.GetPropertycon un nome che non esiste restituiscenull, e la chiamata successiva su di esso lanciaNullReferenceException. Controlla prima di usarlo.BindingFlags.NonPublic | BindingFlags.Instanceraggiunge i membri privati. È legittimo negli strumenti e nei test, ma aggira l'incapsulamento e si rompe senza avvisi quando la classe viene riorganizzata.
Questo ciclo è in sostanza il funzionamento di un esportatore CSV o di un serializzatore da oggetto a JSON: percorri le proprietà, leggi ogni valore, formattalo.
Chiamare metodi e creare oggetti per nome
GetMethod trova un metodo, e Invoke lo chiama con un array di argomenti. Activator.CreateInstance crea un oggetto a partire da un Type, ed è così che i sistemi di plugin e i container DI costruiscono tipi scelti in esecuzione:
Output:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly limita l'elenco ai membri dichiarati nella classe stessa; senza, GetMethods restituisce anche ToString, Equals, GetHashCode e GetType di object. Type.GetType("Name") richiede il nome qualificato con il namespace, e per i tipi in altri assembly anche il nome dell'assembly ("MyApp.Plugins.Csv, MyApp.Plugins").
Se il metodo invocato lancia un'eccezione, Invoke la avvolge in una TargetInvocationException; l'originale si trova nella sua InnerException.
Attributi: etichette lette dal compilatore e dai framework
Un attributo si scrive tra parentesi quadre prima dell'elemento che descrive. Il framework ne definisce molti; eccone alcuni che cambiano ciò che fa il compilatore:
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
Altri vengono letti dalle librerie in esecuzione: [JsonPropertyName] e [JsonIgnore] da System.Text.Json, [Required] e [MaxLength] dalla validazione dei modelli di ASP.NET Core e da Entity Framework, [HttpGet("orders/{id}")] dal routing di ASP.NET, [Fact] e [Test] dai test runner. L'attributo in sé non fa nulla; lo fa il codice che lo cerca.
Il nome ObsoleteAttribute si accorcia in [Obsolete] quando lo applichi: per convenzione il nome di ogni classe di attributo finisce con Attribute, e C# ti permette di omettere il suffisso.
Dichiarare e leggere un attributo personalizzato
Un attributo personalizzato è una classe che deriva da Attribute. [AttributeUsage] dice a cosa può essere applicato. I parametri del costruttore diventano argomenti posizionali, e le proprietà pubbliche impostabili diventano argomenti con nome:
Output:
Username must be at most 20 characters
Email is required
Keep the city code short
0
È una versione in miniatura di ciò che fa la validazione dei modelli di ASP.NET Core con System.ComponentModel.DataAnnotations. Gli argomenti degli attributi devono essere costanti in fase di compilazione (numeri, stringhe, typeof(...), valori di enum o array di questi), perché vengono memorizzati nei metadati dell'assembly. GetCustomAttribute<T>() è un extension method di System.Reflection; esiste anche IsDefined(typeof(T)) quando ti basta sapere se un attributo è presente.
Il costo della reflection
La reflection scambia velocità e sicurezza con flessibilità:
- Velocità. Cercare un membro per nome e chiamarlo tramite
InvokeoGetValueè molto più lento di una chiamata diretta, e fa il boxing dei tipi valore. Per un uso ripetuto, cerca ilPropertyInfoo ilMethodInfouna volta sola e conservalo, oppure trasformalo in un delegate conDelegate.CreateDelegateoMethodInfo.CreateDelegatee chiama quello. - Sicurezza. Un nome scritto male o una firma cambiata compilano senza problemi e falliscono in esecuzione. Preferisci
nameof(Product.Price)alla stringa"Price"ovunque puoi, così le rinominazioni vengono intercettate. - Trimming e AOT. Le app con trimming e Native AOT rimuovono i membri che sembrano non usati da nessuno, e la reflection nasconde l'uso a quell'analisi. Le librerie moderne (
System.Text.Json,GeneratedRegex, logging) stanno passando ai source generator, che fanno lo stesso lavoro in fase di compilazione.
Usa la reflection per le parti di un programma che davvero non conoscono in anticipo i loro tipi: plugin, strumenti generici, serializzatori, helper per i test. Quando i tipi sono noti, il codice normale, i generics o le interfacce sono più veloci e vengono controllati dal compilatore.
Errori comuni
GetType() == typeof(Base)per verificare una classe base. Fallisce per i tipi derivati. UsaisoIsAssignableFrom.- Non controllare
null.GetProperty,GetMethodeType.GetTyperestituiscononullquando non trovano nulla. - Reflection in un ciclo critico senza cache. Metti in cache il
MemberInfoo compila un delegate. - Catturare l'eccezione sbagliata da
Invoke. L'eccezione vera è laInnerExceptiondellaTargetInvocationException. - Stringhe magiche per i nomi dei membri. Usa
nameof.
Domande frequenti
Cos'è la reflection in C#?
La reflection è la capacità di un programma di ispezionare i tipi in esecuzione: elencare le proprietà e i metodi di una classe, leggere e impostare valori per nome, chiamare metodi, creare istanze e leggere attributi. Si trova in System.Reflection e parte da un oggetto Type. Serializzatori, ORM, container di dependency injection e framework di test si basano su di essa.
Che differenza c'è tra typeof e GetType in C#?
typeof(Customer) viene risolto in fase di compilazione a partire dal nome di un tipo e non richiede un oggetto. obj.GetType() si chiama su un'istanza in esecuzione e restituisce il tipo effettivo dell'oggetto, che può essere più derivato del tipo dichiarato della variabile: per Animal a = new Dog();, a.GetType() è Dog. GetType() su un riferimento null lancia NullReferenceException.
Come ottengo il valore di una proprietà per nome in C#?
obj.GetType().GetProperty("Price") restituisce un PropertyInfo (o null se non esiste una proprietà pubblica con quel nome), e .GetValue(obj) la legge come object. .SetValue(obj, value) la scrive. Metti in cache il PropertyInfo se lo fai in un ciclo, perché la ricerca è la parte costosa.
Come creo un attributo personalizzato in C#?
Dichiara una classe che deriva da System.Attribute, dalle un nome con il suffisso Attribute e indica dove può essere usata con [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Applicalo come [MaxLength(50)] e leggilo con property.GetCustomAttribute<MaxLengthAttribute>().
Cosa fa l'attributo Obsolete in C#?
[Obsolete("Use PlaceOrderAsync instead")] su un membro fa sì che il compilatore emetta l'avviso CS0618, con il tuo messaggio, in ogni punto di chiamata. [Obsolete("...", true)] trasforma l'avviso nell'errore CS0619. È il modo in cui le librerie ritirano un'API senza rompere i chiamanti da un giorno all'altro.
La reflection è lenta in C#?
Rispetto a una chiamata diretta sì: trovare un membro per nome e invocarlo tramite MethodInfo.Invoke o PropertyInfo.GetValue è in genere da decine a centinaia di volte più lento, e fa il boxing dei tipi valore. Va bene all'avvio, per la configurazione e per un uso occasionale. Per i percorsi critici metti in cache il MemberInfo, costruisci un delegate una volta sola oppure usa i generics o un source generator.