1. Introduzione
La riflessione (dall'inglese Reflection) è la capacità del tuo programma di esplorare la propria struttura durante l'esecuzione. Immagina che il tuo programma possa chiedersi: 'Quali metodi ho? Quali proprietà ha questo tipo?' e ottenere una risposta a runtime.
Esempi di utilizzo della reflection nella vita reale
- Creazione di plugin e moduli: Non serve conoscere a priori con quale classe lavorerai — basta caricare la DLL e scoprire i suoi metodi al volo.
- Automazione dei test: I framework di test, per esempio xUnit e NUnit, trovano i metodi di test tramite reflection.
- Serializzazione: Con la reflection puoi trasformare automaticamente un qualsiasi oggetto in JSON/XML senza enumerare manualmente le proprietà.
- Validazione dei dati tramite attributi: Per esempio, se hai aggiunto l'attributo [Required] a una proprietà — puoi verificare automaticamente tutte le proprietà così annotate.
Perché è utile saperlo per i colloqui
La capacità di usare la reflection viene spesso richiesta nei colloqui perché dimostra la comprensione dell'architettura .NET e l'abilità di risolvere problemi dinamici. Se vuoi scrivere i tuoi framework o estensioni, la reflection è indispensabile.
2. Spazio dei nomi e classi base della reflection
Tutte le funzionalità della reflection in C# sono disponibili tramite lo spazio dei nomi System.Reflection. All'inizio del file non dimenticare:
using System.Reflection;
Tipi principali della reflection
| Classe/Tipo | Descrizione |
|---|---|
|
Rappresenta la descrizione di un tipo in .NET (per esempio, classe, interface, enum, ecc.) |
|
Proprietà di un tipo |
|
Metodo di un tipo |
|
Campo di un tipo |
|
Costruttore di un tipo |
|
Rappresenta un assembly (EXE o DLL) |
Metodi principali della classe Type
| Metodo | Descrizione |
|---|---|
|
Restituisce tutte le proprietà pubbliche |
|
Restituisce tutti i campi pubblici |
|
Restituisce tutti i metodi pubblici |
|
Restituisce tutti i costruttori pubblici |
|
Restituisce tutte le interfacce che il tipo implementa |
|
Restituisce tutti gli attributi applicati al tipo |
Spesso a questi metodi puoi passare BindingFlags per accedere a membri privati o statici, ma per ora ci limitiamo allo scenario base.
3. Primo passo: ottenere un oggetto Type
Tutte le tecniche della reflection si basano sul lavoro con un oggetto di tipo Type.
Ci sono diversi modi per ottenere questo oggetto, vediamoli:
Metodo 1: Tramite un oggetto
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
Metodo 2: Tramite il nome del tipo
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
Metodo 3: Da stringa (dinamicamente)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
Attenzione! Per le classi personalizzate in altri assembly è necessario specificare il nome completo del tipo includendo l'assembly.
4. Esplorare la struttura del tipo: proprietà, metodi, campi e costruttori
È arrivato il momento della verità: abbiamo ottenuto l'oggetto Type — e ora possiamo scoprire tutto quello che vogliamo.
Ottenere la lista delle proprietà
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
Output:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
Ottenere la lista dei metodi
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
I tipi grandi possono avere moltissimi metodi! Spesso ti interessano non tutti, ma solo i "tuoi" (per esempio escludendo i metodi ereditati da object). Ne parleremo più avanti.
Ottenere la lista dei campi
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
Ma la maggior parte delle classi normali non ha campi pubblici, perché l'incapsulamento è importante :)
Ottenere i costruttori
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"Costruttore: {ctor}");
}
5. Applichiamo la reflection nella pratica: analizziamo la nostra app
Ricordo che nel nostro progetto didattico esiste la classe TaskItem, dove memorizziamo i task. Vediamola con la reflection.
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
Ecco come ottenere tutti i dettagli del nostro tipo a runtime:
Type taskType = typeof(TaskItem);
Console.WriteLine("Proprietà:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("Campi:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("Metodi:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
Prova ad aggiungere nuove proprietà alla classe e guarda come la reflection le rileva. Così funzionano i serializer universali e gli object inspector.
6. Visualizzazione: com'è fatto l'oggetto Type (schema)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
Questo è il "ritratto del tipo" — ha un nome, un assembly, un insieme di membri e attributi.
Ottenere informazioni di base sul tipo
L'oggetto Type può dire che tipo è:
Type t = typeof(double);
Console.WriteLine(t.Name); // Double
Console.WriteLine(t.FullName); // System.Double
Console.WriteLine(t.Namespace); // System
Console.WriteLine(t.IsClass); // False
Console.WriteLine(t.IsValueType); // True
Console.WriteLine(t.IsEnum); // False
Console.WriteLine(t.IsPrimitive); // True
7. Cercare una proprietà, un metodo o un campo per nome
Proprietà
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"Tipo della proprietà 'Title': {prop.PropertyType}");
}
Metodo
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"Il metodo 'ToString' restituisce: {method.ReturnType}");
}
Campo
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"Tipo del campo 'IsCompleted': {field.FieldType}");
}
8. Usare la reflection per accesso dinamico ai valori
Ora non ci limitiamo a guardare, ma possiamo anche toccare i valori!
Leggere il valore di una proprietà
TaskItem item = new TaskItem { Id = 1, Title = "Testare la reflection", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"Titolo: {value}");
}
Impostare il valore di una proprietà
titleProp.SetValue(item, "Titolo modificato");
Console.WriteLine(item.Title);
Lavorare con i campi
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
Per campi e proprietà privati servono flag speciali, per esempio BindingFlags.NonPublic. Ne parleremo più avanti.
9. Invocare metodi tramite reflection
Puoi invocare metodi anche senza conoscerli a compile-time!
TaskItem task = new TaskItem { Title = "Reflection — bella!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
Se il metodo ha parametri, devi passarli come array di oggetti:
public class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
var addMethod = typeof(Calculator).GetMethod("Add");
object[] parameters = { 5, 3 };
object sumResult = addMethod.Invoke(calc, parameters);
Console.WriteLine(sumResult); // 8
10. Creare oggetti dinamicamente tramite costruttore
La reflection permette di creare oggetti anche senza l'operatore new!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// È lo stesso TaskItem (ma il tipo è object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
È possibile passare argomenti al costruttore:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "Elis");
Console.WriteLine(((User)user).Name); // Elis
11. Errori tipici quando si lavora con System.Reflection
Errore №1: Tentare di accedere a membri privati senza BindingFlags.
Per default la reflection vede solo i membri pubblici. Senza BindingFlags.NonPublic l'accesso a campi o metodi privati non è possibile.
Errore №2: Ignorare il controllo su null.
I metodi GetProperty, GetMethod e altri possono restituire null se il membro non è trovato. Senza controllo questo porta a NullReferenceException.
Errore №3: Scelta errata della overload del metodo.
Se ci sono più overload di un metodo, GetMethod potrebbe restituire la variante sbagliata. Usa GetMethod specificando i tipi dei parametri.
Errore №4: Usare la reflection in codice ad alte prestazioni.
La reflection è più lenta delle chiamate dirette. Per codice ad alta performance usa caching dei metadati o delegate.
GO TO FULL VERSION