Sei sulla pagina 1di 9

Programmazione Orientata agli Oggetti

7. Reflection

Reflection (riflessione) è un termine generico per indicare la capacità di introspezione (= guardarsi


dentro) degli oggetti, cioè la possibilità di ispezionare e manipolare i metadati di un programma a
tempo di esecuzione.
Per metadati si intendono le informazioni che descrivono i dati cioè ad esempio le classi utilizzate nel
programma.
Possiamo quindi usare la reflection per analizzare le classi capendo quali sono le proprietà di cui di-
spongono. È possibile invocare i metodi degli oggetti oppure capire da quale classe base deriva una
certa classe. La reflection permette anche di modificare classi o definire nuove classi dal nulla a run-
time, compilandole in RAM e istanziando oggetti delle nuove classi.
Più precisamente, gli assembly contengono moduli, che contengono tipi, che a loro volta contengono
membri. La reflection fornisce oggetti che incapsulano assembly, moduli e tipi. È possibile utilizzare la
reflection per creare in modo dinamico un’istanza di un tipo, associare il tipo a un oggetto esistente o
ottenere il tipo da un oggetto esistente. È quindi possibile richiamare i metodi del tipo o accedere ai
relativi campi e proprietà.

Abbiamo già incontrato in precedenza la classe System.Type che è la classe fondamentale per la re-
flection. Quando la reflection lo richiede, il CLR crea l’oggetto Type relativo a un tipo caricato.
Per ottenere informazioni sul tipo, è possibile utilizzare metodi, campi, proprietà e classi nidificate
dell’oggetto Type.
Queste ultime sono definite nel namespace System.Reflection (https://docs.microsoft.com/it-
it/dotnet/api/system.reflection).

Alcune classi contenute in questo namespace sono:


 Assembly: per definire e caricare un assembly, caricare i moduli elencati nel manifesto
dell’assembly e individuare un tipo dell’assembly e crearne un’istanza
 Module: per individuare informazioni quali l’assembly che contiene il modulo e le classi del
modulo (per modulo si intende il file eseguibile, ad esempio un file .exe o .dll; uno o più moduli
compongono un assembly)
 ConstructorInfo: per ottenere informazioni sui costruttori
 MethodInfo: per ottenere informazioni sui metodi
 FieldInfo: per ottenere informazioni sui campi
 EventInfo: per ottenere informazioni sugli eventi
 PropertyInfo: per ottenere informazioni sulle proprietà
 ParameterInfo: per ottenere informazioni sui parametri di un metodo

La reflection può essere utilizzata anche per creare applicazioni definite visualizzatori di tipi, che con-
sentono agli utenti di selezionare i tipi e visualizzarne quindi le relative informazioni (come quello usa-
to dall’IDE di sviluppo Visual Studio).

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 1
Programmazione Orientata agli Oggetti

7.1. Utilizzo della classe Type


Per ottenere informazioni su un tipo è necessario creare un oggetto di tipo Type. Una volta dichiarato
una variabile t di tipo Type:
Type t;

la creazione di un oggetto di tipo Type può essere fatta in tre modi (facciamo riferimento alle classi di
esempio Persona, Studente e Docente le ultime due derivate dalla prima):

1. utilizzando l’operatore typeof() che riceve come parametro il tipo da esaminare:


t = typeof(Persona);

2. utilizzando il metodo GetType() di Object chiamato su un’istanza della classe da esaminare:


Studente s = new Studente("Collauto", "Guido", "4D1");
t = s.GetType();

3. utilizzando il metodo statico Type.GetType() che riceve una stringa con il nome del tipo da
ispezionare:
t = Type.GetType("DemoReflection.Docente");

A questo punto possiamo utilizzare vari metodi della classe Type per ottenere le informazioni sul tipo.
Anzi, scriviamo un metodo che riceve un parametro di tipo Type e restituisce una stringa con varie in-
formazioni sul tipo:
private string OttieniInformazioniTipo(Type t)
{
string msg = "";
if (t == null)
msg += "\r\nTipo nullo";
else
{
ricaviamo nome, spazio di nomi e nome completo (dato da namespace.nome)
msg += "\r\nNome tipo: " + t.Name;
msg += "\r\nSpazio di nomi: " + t.Namespace;
msg += "\r\nNome completo: " + t.FullName;
ricaviamo il tipo dal quale deriva il tipo esaminato:
Type tipoBase = t.BaseType;
if (tipoBase != null)
msg += "\r\nTipo base: " + tipoBase.Name;

e altre informazioni (booleane) sul tipo:


msg += "\r\nAstratto: " + t.IsAbstract;
msg += "\r\nClasse: " + t.IsClass;
msg += "\r\nEnumerativo: " + t.IsEnum;
msg += "\r\nPrimitivo: " + t.IsPrimitive;
msg += "\r\nTipo valore: " + t.IsValueType;

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 2
Programmazione Orientata agli Oggetti

quindi, passiamo ad elencare tutti i membri pubblici del tipo (costruttori, campi, metodi e proprietà)
attraverso l’utilizzo del metodo GetMembers() che restituisce un array di oggetti di tipo MemberInfo
per mezzo dei quali possiamo ricavare informazioni sul membro come ad esempio tipo di membro
(Method, Property, Constructor, Field,...) e nome:
msg += "\r\nMEMBRI PUBBLICI: ";
MemberInfo[] membriPubblici = t.GetMembers();
foreach (MemberInfo m in membriPubblici)
msg += "\r\n" + m.MemberType + " " + m.Name;
anziché usare il metodo GetMembers() possiamo usare GetFields() per ottenere informazioni solo sui
campi:
msg += "\r\nCAMPI PUBBLICI: ";
FieldInfo[] campi = t.GetFields();
foreach (FieldInfo f in campi)
msg += "\r\n" + f.Name;
oppure sui costruttori sfruttando il metodo GetConstructors() che restituisce un array di oggetti di ti-
po ConstructorInfo. Per ogni costruttore è possibile utilizzare il metodo GetParameters() per ottenere
informazioni sui parametri del costruttore attraverso oggetti di classe ParameterInfo:
msg += "\r\nCOSTRUTTORI: ";
ConstructorInfo[] costruttori = t.GetConstructors();
foreach (ConstructorInfo c in costruttori)
{
// ricava i parametri dei costruttori
string s = "";
ParameterInfo[] parametri = c.GetParameters();
foreach (ParameterInfo pi in parametri)
s += pi.ToString() + " ";

if (s == "") s = "senza parametri";


else s = " con parametri " + s;
msg += "\r\nCostruttore " + s;
}
in modo analogo possiamo ottenere informazioni sui metodi utilizzando GetMethods() che restituisce
un array di MethodInfo; come per i costruttori ricaviamo anche in questo caso l’elenco dei parametri di
ogni metodo:
msg += "\r\nMETODI PUBBLICI: ";
MethodInfo[] metodi = t.GetMethods();
foreach (MethodInfo m in metodi)
{
// ricava i parametri dei metodi
string s = "";
ParameterInfo[] parametri = m.GetParameters();
foreach (ParameterInfo pi in parametri)
s += pi.ToString() + " ";

if (s == "") s = " senza parametri";


else s = " con parametri " + s;
msg += "\r\nMetodo " + m.Name + s;
}
}
return msg;
}

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 3
Programmazione Orientata agli Oggetti

Richiamando il codice precedente sul tipo Studente otteniamo il seguente output:


Nome tipo: Studente CAMPI PUBBLICI:
Spazio di nomi: DemoReflection
Nome completo: DemoReflection.Studente COSTRUTTORI:
Tipo base: Persona Costruttore senza parametri
Astratto: False Costruttore con parametri System.String
Classe: True cognome System.String nome System.String
Enumerativo: False classe
Primitivo: False METODI PUBBLICI:
Tipo valore: False Metodo get_Classe senza parametri
MEMBRI PUBBLICI: Metodo set_Classe con parametri
Method get_Classe System.String value
Method set_Classe Metodo ToString senza parametri
Method ToString Metodo get_Cognome senza parametri
Method get_Cognome Metodo set_Cognome con parametri
Method set_Cognome System.String value
Method get_Nome Metodo get_Nome senza parametri
Method set_Nome Metodo set_Nome con parametri System.String
Method Equals value
Method GetHashCode Metodo Equals con parametri System.Object
Method GetType obj
Constructor .ctor Metodo GetHashCode senza parametri
Constructor .ctor Metodo GetType senza parametri
Property Classe
Property Cognome
Property Nome

7.2. Ricavare i tipi definiti in un assembly


Possiamo sfruttare la classe Assembly per ottenere l’elenco dei tipi definiti in un determinato assem-
bly. Ad esempio, supposto di voler popolare un ComboBox con i tipi derivati dalla classe Persona, po-
tremo definire l’evento click del pulsante in questo modo:

private void btnPopolaComboBox_Click(object sender, EventArgs e)


{
Type t = typeof(Persona);

// ottiene l'assembly in cui è definito il tipo Persona


Assembly asm = t.Assembly;

// ottiene l'elenco dei tipi definiti nell'assembly


Type[] tipi = asm.GetTypes();

cbxClassi.Items.Clear();
foreach (Type tipo in tipi)
// se il tipo è derivato direttamente (o indirettamente) da Persona
if (tipo.IsSubclassOf(t))
cbxClassi.Items.Add(tipo.FullName); // lo aggiunge al ComboBox

// oppure per ottenere solo le derivazioni dirette


// if (tipo.BaseType == t) ...
}

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 4
Programmazione Orientata agli Oggetti

7.3. Creare un oggetto di un certo tipo a tempo di esecuzione


Vogliamo ora sfruttare la scelta effettuata dall’utente attraverso il ComboBox per creare un oggetto
del tipo desiderato:

Sfruttiamo il metodo statico Type.GetType() per ottenere, nella variabile t, il riferimento al tipo scelto
in base alla stringa che ne contiene il nome (la scelta fatta nel ComboBox), quindi usiamo la classe Sy-
stem.Activator ed il suo metodo CreateInstance() per creare una istanza del tipo t:

private void btnCreaOggetto_Click(object sender, EventArgs e)


{
// ottiene il riferimento al tipo in base al nome (stringa)
Type t = Type.GetType(cbxClassi.Text);

Persona p;
if (t != null)
{
// Activator contiene metodi per creare tipi di oggetti
// è necessario il cast perchè Activator crea un oggetto di tipo Object
p = Activator.CreateInstance(t) as Persona;
if (p != null)
MessageBox.Show("Oggetto di tipo "+
p.GetType().Name + " creato a run-time con successo");
}
}

Nota: una implementazione alternativa poteva essere quella di sostituire l’istruzione all’interno del ci-
clo foreach del metodo btnPopolaComboBox_Click con cbxClassi.Items.Add(tipo) (aggiungen-
do cioè direttamente il riferimento all’oggetto Type).
Quindi nel metodo btnCreaOggetto_Click precedente sostituire la prima istruzione con

Type t = cbxClassi.SelectedItem as Type

Il metodo Activatore.CreateInstance() ha diversi overload ad esempio per specificare i parametri del


costruttore da usare per creare l’oggetto oppure per specificare due stringhe, una per il namespace e
l’altra per il nome del tipo, al posto del riferimento all’oggetto Type.

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 5
Programmazione Orientata agli Oggetti

Con la chiamata che abbiamo usato nel codice precedente viene invocato il costruttore senza parame-
tri del tipo t, per cui se questo non esiste, l’istruzione solleva un’eccezione. Assumiamo di aver inserito
nelle classi Studente e Docente un costruttore senza parametri.

Un altro modo per creare oggetti del tipo t può sfruttare il metodo, della classe Type,
GetConstructor() che restituisce un oggetto di tipo CostructorInfo. Sull’oggetto restituito può essere
richiamato il metodo Invoke() che esegue il costruttore.
Il metodo GetConstructor() cerca un costruttore che abbia la firma specificata; la firma viene
specificata attraverso un array di tipo Type. Se viene passato un array vuoto (attenzione: non nullo ma
di zero elementi) verrà cercato il costruttore senza parametri.
In conclusione il codice precedente, che usava CreateInstance(), può essere sostituito dal seguente:
ConstructorInfo ctr;

// ottiene il riferimento al costruttore senza parametri


ctr = t.GetConstructor(new Type[] { });

// richiama il costruttore senza parametri per creare un oggetto di tipo t


p = ctr.Invoke(null) as Persona;

a titolo di esempio, supposto che le classi abbiano un costruttore con due parametri di tipo stringa, per
invocarlo dovremo aver scritto un codice come il seguente:
// ottiene il riferimento al costruttore a due parametri stringa
ctr = t.GetConstructor(new Type[] { typeof(string), typeof(string) });

// invoca il costruttore passando i parametri (array di object)


p = ctr.Invoke(new object[] { "Collauto", "Guido" }) as Persona;

Infine, un’ulteriore strada, simile alla precedente, è quella di usare il metodo GetConstructors() che,
come abbiamo visto nel primo paragrafo, restituisce un array di tipo CostructorInfo con tutti i costrut-
tori della classe.
I costruttori sono elencati nell’array nell’ordine in cui sono stati definiti nella classe; se quindi, ad
esempio, il costruttore senza parametri è il primo, possiamo usare le seguenti istruzioni per richiamar-
lo:
ctr = t.GetConstructors()[0];
p = ctr.Invoke(null) as Persona;

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 6
Programmazione Orientata agli Oggetti

7.4. Compilare una nuova classe a tempo di esecuzione


Strettamente collegata alla reflection c’è la possibilità di invocare il compilatore C# a run-time compi-
lando cioè codice a tempo di esecuzione.
Nella seguente applicazione illustrerò come sfruttare questa funzionalità per valutare e calcolare
un’espressione matematica inserita dall’utente come stringa.
Chi si è cimentato con l’implementazione di un programma per il parsing di espressioni matematiche
sa che non è un’impresa semplice visto che si devono isolare i termini dell’espressione, considerare la
priorità degli operatori matematici, la presenza di parentesi, di funzioni, …
Definiremo il sorgente per una classe che contiene un’unica funzione statica la cui espressione sarà
quella inserita dall’utente. Dopodiché andremo a compilare questa classe e quindi ad invocarla con
l’eventuale parametro inserito dall’utente (per calcolare anche espressioni del tipo f(x)).

I namespace che dobbiamo includere, oltre a System.Reflection, sono Microsoft.CSharp


(https://docs.microsoft.com/it-it/dotnet/api/microsoft.csharp) e System.CodeDom.Compiler
(https://docs.microsoft.com/it-it/dotnet/api/system.codedom.compiler).

Scriviamo un metodo che riceve un’espressione, definisce una classe statica contenente un metodo
apposito che esegue l’espressione, compila la classe e ricava, dal codice compilato, un riferimento al
metodo:
public static MethodInfo CompilaEspressione(string espressione)
{
// esempio di codice per sistemazione l'espressione secondo la sintassi C#
espressione = espressione.ToLower();
espressione = espressione.Replace("sen", "Math.Sin");
espressione = espressione.Replace("pi", "Math.PI");

// codice della classe da compilare, che contiene l'espressione della funzione


string codiceClasse = "using System;"
+ "public static class Funzione {"
+ "public static void Main() { Console.WriteLine(\"Non esisto!\"); } "
+ "public static double f(double x) { return "+ espressione +"; }}";

// N.B.: non è necessario inserire il metodo Main se GenerateExecutable = false

// imposta i parametri di compilazione


CompilerParameters parametriCompilazione = new CompilerParameters();

// eventualmente si possono impostare vari flag


// (i seguenti sono di default tutti a false)
parametriCompilazione.GenerateInMemory = false;
parametriCompilazione.IncludeDebugInformation = false;
parametriCompilazione.GenerateExecutable = true; // viene generato un file
temporaneo .exe anzichè un file temporaneo .dll

// richiama l'istanza del compilatore


CodeDomProvider compilatore = CodeDomProvider.CreateProvider("CSharp");

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 7
Programmazione Orientata agli Oggetti

// compila la classe definita da codiceClasse


CompilerResults risultatiCompilazione =
compilatore.CompileAssemblyFromSource(parametriCompilazione, codiceClasse);

// verifica se ci sono errori di compilazione


if (risultatiCompilazione.Errors.Count > 0)
{
string messaggi = "";
foreach(CompilerError errore in risultatiCompilazione.Errors)
messaggi += errore.ErrorText + "\n";

throw new Exception("L'espressione contiene i seguenti errori:\n"


+ messaggi);
}
else
MessageBox.Show($"Sorgente compilato in
{risultatiCompilazione.PathToAssembly} \n\n
{risultatiCompilazione.TempFiles.Count}
file temporanei creati durante la compilazione");

// ricava un riferimento all'assembly compilato


Assembly asm = risultatiCompilazione.CompiledAssembly;

// ... e al metodo di nome f della classe Funzione


MethodInfo metodo = asm.GetType("Funzione").GetMethod("f");

// ritorna il riferimento al metodo


return metodo;
}

Sfrutteremo il metodo precedente nella nostra applicazione, di cui è mostrata l’interfaccia grafica:
l’utente inserisce la funzione nel controllo cbxFunzione e clicca sul pulsante “Compila espressione”
per generare il metodo che verrà poi invocato ad ogni calcolo dell’espressione.

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 8
Programmazione Orientata agli Oggetti

Dichiariamo un attributo globale di tipo MethodInfo che conterrà il riferimento al metodo compilato:

static MethodInfo funzione = null;

private void btnCompila_Click(object sender, EventArgs e)


{
try
{
funzione = CompilaEspressione(cbxFunzione.Text);
lblFunzione.Text = cbxFunzione.Text;
}
catch (Exception errore)
{
funzione = null;
MessageBox.Show(errore.Message);
}
}

Se non vi sono stati errori di sintassi nell’espressione, il metodo è stato compilato e abbiamo un suo
riferimento nella variabile funzione.
A questo punto l’utente può calcolarne il valore cliccando sul pulsante “Calcola espressione” il quale
richiama un metodo CalcolaEspressione che fa uso del metodo Invoke. Il metodo Invoke esegue il
metodo e riceve un primo parametro che è l’oggetto sul quale eseguire il metodo (nel nostro caso è
null perchè il metodo è statico) ed un secondo parametro che è una lista di Object contenenti i para-
metri del metodo (nel nostro caso uno solo, il valore di x):

public static double CalcolaEspressione(double x)


{
object[] parametriFunzione = new object[] { x };
return (double)funzione.Invoke(null, parametriFunzione);
}

private void btnCalcola_Click(object sender, EventArgs e)


{
if (funzione == null)
MessageBox.Show("La funzione non è stata compilata");
else
{
double x = Convert.ToDouble(txtX.Text);
MessageBox.Show($"f({x}) = {CalcolaEspressione(x)}");
}
}

Lezioni di Informatica (tutti i diritti riservati) – prof. Riccardo Crosato @ IIS “Marzotto-Luzzatti” 9

Potrebbero piacerti anche