Fins ara, tot el codi de BiblioTech ha conegut en temps de compilació quines propietats i
mètodes té cada classe: en escriure llibre1.Titol, el compilador comprova que Llibre té una
propietat Titol abans de generar el programa. La reflexió (System.Reflection) inverteix
aquesta idea: permet que un programa inspeccioni i manipuli els seus propis tipus en temps
d'execució, sense conèixer-los d'antuvi. És, literalment, el mecanisme que fa possible que
JsonSerializer (Mòdul 5) sàpiga quines propietats té un Llibre sense que ningú hagi escrit
codi específic de mapeig per a aquesta classe: per sota, recorre les seves propietats per
reflexió. Aquesta lliçó obre el Mòdul 6 explicant com funciona aquest mecanisme i com fer-lo
servir directament.
Contingut
- Què és la reflexió i per a què serveix
- Obtenir un
Type:typeofiGetType() - Inspeccionar propietats i mètodes:
GetProperties()iGetMethods() - Llegir i escriure valors de propietats per reflexió
- Crear instàncies dinàmicament amb
Activator.CreateInstance MostrarPropietats: un inspector genèric per al domini de BiblioTech- Cost de la reflexió i casos d'ús reals
- Què és la reflexió i per a què serveix
La reflexió és la capacitat d'un programa d'examinar la seva pròpia estructura —classes,
propietats, mètodes, atributs— com si fossin dades, i d'actuar-hi sense que el codi que la fa
servir conegui d'antuvi el tipus concret amb què treballa. Tot el namespace System.Reflection
gira al voltant d'un tipus central: System.Type, que representa, en temps d'execució, "la
descripció completa d'una classe" (les seves propietats, els seus mètodes, la seva classe base,
les seves interfícies...).
La reflexió no és una eina que es faci servir cada dia en el codi de domini d'una aplicació com
BiblioTech (Llibre.Titol es continua escrivint directament, sense reflexió, en el 99% del
codi). El seu valor apareix en codi genèric i d'infraestructura: frameworks que necessiten
treballar amb "qualsevol tipus" sense conèixer-lo per endavant. Ja has fet servir diverses peces
construïdes així sense saber-ho:
| Eina ja vista al curs | Què fa per sota amb reflexió |
|---|---|
JsonSerializer.Serialize/Deserialize (Mòdul 5) |
Recorre les propietats públiques d'un tipus per llegir-les o assignar-les |
Entity Framework Core (DbSet<T>) |
Inspecciona les propietats de Llibre, Soci... per mapejar-les a columnes |
| Frameworks d'injecció de dependències (Mòdul 8) | Creen instàncies de classes el tipus concret de les quals no coneixen en temps de compilació |
- Obtenir un
Type: typeof i GetType()
Type: typeof i GetType()Hi ha dues maneres d'obtenir el Type que representa una classe, segons si el tipus es coneix
en temps de compilació o només es disposa d'una instància en temps d'execució:
using System;
// Forma 1: typeof, quan el tipus es coneix en temps de compilacio
Type tipusLlibre = typeof(Llibre);
// Forma 2: GetType(), quan nomes es te una instancia (el tipus pot no coneixer-se d'antuvi)
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Type tipusDesDeInstancia = llibre1.GetType();
Console.WriteLine(tipusLlibre == tipusDesDeInstancia); // True: tots dos representen el mateix Type
Console.WriteLine(tipusLlibre.Name); // "Llibre"
Console.WriteLine(tipusLlibre.FullName); // el nom complet, amb namespacetypeof(T) |
objecte.GetType() |
|
|---|---|---|
| Quan es fa servir | El tipus es coneix al codi, escrit literalment | Només es disposa d'una referència en temps d'execució |
| Resultat amb polimorfisme | El tipus exacte indicat entre parèntesis | El tipus real de l'objecte, encara que la variable sigui d'un tipus més general |
El segon matís és important: si MaterialBibliotecari material = llibre1; (recordant el
Polimorfisme del Mòdul 3), material.GetType() retorna Llibre, no MaterialBibliotecari
—GetType() sempre revela el tipus concret de l'objecte en memòria, sense importar el tipus
de la variable que el referencia.
- Inspeccionar propietats i mètodes:
GetProperties() i GetMethods()
GetProperties() i GetMethods()Un Type exposa, entre molts altres, els mètodes GetProperties() i GetMethods(), que
retornen arrays de PropertyInfo i MethodInfo respectivament: objectes que descriuen, un per
un, cada propietat o mètode públic del tipus.
using System.Reflection;
Type tipusLlibre = typeof(Llibre);
PropertyInfo[] propietats = tipusLlibre.GetProperties();
foreach (PropertyInfo propietat in propietats)
{
Console.WriteLine($"Propietat: {propietat.Name} (tipus {propietat.PropertyType.Name})");
}
// Propietat: Titol (tipus String)
// Propietat: Autor (tipus String)
// Propietat: Disponible (tipus Boolean)
// Propietat: Isbn (tipus String)
MethodInfo[] metodes = tipusLlibre.GetMethods();
foreach (MethodInfo metode in metodes)
{
Console.WriteLine($"Metode: {metode.Name}");
}
// Metode: Prestar, Retornar, MostrarFitxa, Descriure, Coincideix, GetType, ToString... (heretats inclosos)GetProperties() sobre typeof(Llibre) inclou tant les propietats declarades directament a
Llibre (Isbn) com les heretades de MaterialBibliotecari (Titol, Autor, Disponible):
la reflexió respecta la jerarquia d'herència igual que el propi llenguatge. GetMethods(), per
defecte, també inclou mètodes heretats d'object (ToString(), GetType(), Equals()...),
cosa que convé tenir present en recórrer el resultat.
- Llegir i escriure valors de propietats per reflexió
Un PropertyInfo no és només una descripció: també permet llegir (GetValue) o
escriure (SetValue) el valor d'aquesta propietat sobre una instància concreta, passada com
a argument:
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
PropertyInfo? propietatTitol = typeof(Llibre).GetProperty("Titol");
object? valorTitol = propietatTitol?.GetValue(llibre1);
Console.WriteLine(valorTitol); // "Rayuela"
propietatTitol?.SetValue(llibre1, "Rayuela (edicio revisada)");
Console.WriteLine(llibre1.Titol); // "Rayuela (edicio revisada)"GetProperty("Titol") cerca, pel seu nom com a cadena de text, la propietat concreta Titol;
retorna null si no existeix cap amb aquest nom exacte, d'aquí el ? a GetValue. Això és
justament el que distingeix la reflexió d'accedir a llibre1.Titol directament: el nom de la
propietat pot ser una dada (una cadena que arriba des de configuració, des d'un fitxer, o
que es calcula en un bucle), no un identificador fix escrit al codi.
Disponible, amb el seu private set (Mòdul 3), continua respectant el seu encapsulament
davant SetValue per defecte: GetProperty("Disponible")?.SetValue(llibre1, false) llança una
excepció, tret que se sol·liciti explícitament accés a membres no públics amb
BindingFlags.NonPublic, una possibilitat que existeix però que convé evitar —saltar-se un
private set per reflexió trenca precisament la garantia que aquest private set pretenia
donar.
- Crear instàncies dinàmicament amb
Activator.CreateInstance
Activator.CreateInstanceA més d'inspeccionar tipus ja instanciats, la reflexió permet crear instàncies noves d'un
tipus que només es coneix en temps d'execució (per exemple, a partir del seu nom com a cadena de
text), amb Activator.CreateInstance:
using System;
Type tipusSoci = typeof(Soci);
// Activator.CreateInstance necessita els arguments exactes d'algun constructor de Soci
object? nouSoci = Activator.CreateInstance(tipusSoci, 99, "Soci Creat Dinamicament");
if (nouSoci is Soci soci)
{
Console.WriteLine($"{soci.Id}: {soci.Nom}"); // 99: Soci Creat Dinamicament
}Activator.CreateInstance(tipus, arguments...) cerca, entre els constructors públics de
tipus, un la signatura del qual coincideixi amb els arguments indicats, i l'invoca; el
resultat es retorna com a object, per la qual cosa cal un is/as (Mòdul 3) per tornar-lo a
tractar com a Soci amb tots els seus membres. Aquest mecanisme és la base de molts frameworks:
un contenidor d'injecció de dependències (esmentat de passada aquí; es retoma al Mòdul 8) rep,
en temps d'execució, una llista de tipus a instanciar sense conèixer-los en temps de compilació,
i fa servir Activator.CreateInstance (o mecanismes equivalents més optimitzats) per
construir-los.
MostrarPropietats: un inspector genèric per al domini de BiblioTech
MostrarPropietats: un inspector genèric per al domini de BiblioTechUnint GetType(), GetProperties() i GetValue(), es pot escriure una única funció que
imprimeix el nom i el valor de cada propietat pública de qualsevol objecte, sense necessitat
d'un mètode MostrarFitxa() diferent per a cada classe del domini:
using System.Reflection;
static void MostrarPropietats(object obj)
{
Type tipus = obj.GetType();
Console.WriteLine($"--- {tipus.Name} ---");
foreach (PropertyInfo propietat in tipus.GetProperties())
{
object? valor = propietat.GetValue(obj);
Console.WriteLine($"{propietat.Name}: {valor}");
}
}Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Soci soci1 = new Soci(1, "Ana Martinez");
MostrarPropietats(llibre1);
// --- Llibre ---
// Titol: Rayuela
// Autor: Julio Cortazar
// Disponible: True
// Isbn: 978-84-376-0495-4
MostrarPropietats(soci1);
// --- Soci ---
// Id: 1
// Nom: Ana MartinezMostrarPropietats rep object obj (el tipus més general possible, Mòdul 3) precisament perquè
no necessita saber d'antuvi si rebrà un Llibre, una Revista o un Soci: fa servir
obj.GetType() per descobrir-ho en temps d'execució, i a partir d'aquí recorre les seves
propietats de forma genèrica. És una eina de depuració molt pràctica: un únic mètode serveix per
inspeccionar l'estat intern de qualsevol objecte del domini, sense escriure ni mantenir un
MostrarFitxa() a mida per cada classe nova que s'afegeixi a BiblioTech.
flowchart LR
A["MostrarPropietats(obj)"] --> B["obj.GetType()"]
B --> C["tipus.GetProperties()"]
C --> D["Per cada PropertyInfo: propietat.GetValue(obj)"]
D --> E["Console.WriteLine(nom + valor)"]
- Cost de la reflexió i casos d'ús reals
La reflexió té un cost de rendiment notable davant l'accés directe (llibre1.Titol): localitzar
un membre pel seu nom, comprovar tipus i signatures en temps d'execució és sensiblement més lent
que una crida resolta en temps de compilació. Això no vol dir que s'hagi d'evitar sempre, sinó
que cal reservar-la per a on de veritat aporta valor:
| Cas d'ús | Per què encaixa amb reflexió |
|---|---|
Serialitzadors genèrics (JsonSerializer) |
Necessiten funcionar amb qualsevol classe, sense codi específic per tipus |
| Frameworks d'injecció de dependències (Mòdul 8) | Creen instàncies de tipus configurats externament, no coneguts en temps de compilació |
Eines de depuració/inspecció (MostrarPropietats) |
El benefici de la inspecció genèrica compensa el cost, perquè no s'executen al camí crític de rendiment |
Codi de domini quotidià (llibre1.Prestar()) |
No encaixa: l'accés directe és més ràpid, més segur en temps de compilació i més llegible |
Errors Comuns i Consells
- Fer servir reflexió on l'accés directe ja funciona: si el tipus es coneix en temps de
compilació (el cas habitual en el codi de domini de BiblioTech), accedir directament a
llibre1.Titolés més ràpid, més segur i més llegible que cercar-lo per reflexió amb una cadena de text. - Oblidar comprovar
nullaGetProperty/GetMethod: si el nom cercat no existeix (per exemple, per una errata a la cadena de text), aquests mètodes retornennullen lloc de llançar una excepció; accedir a.GetValue(...)sobre aquestnullsense comprovar-ho llança unaNullReferenceException. - Saltar-se l'encapsulament amb
BindingFlags.NonPublicsense necessitat real: és tècnicament possible llegir o escriure membresprivateper reflexió, però fer-ho rutinàriament trenca les garanties de disseny (com elprivate setdeDisponible) que el propi codi va establir a propòsit. - No mesurar l'impacte en rendiment abans de fer servir reflexió en un camí crític: en un
bucle que s'executa milions de vegades, la diferència entre accés directe i reflexió pot ser
significativa; per a codi d'infraestructura molt sensible al rendiment existeixen alternatives
més avançades (compilació d'expressions,
Source Generators) fora de l'abast d'aquest curs. - Consell: abans d'escriure codi amb reflexió, pregunta't si el problema es pot resoldre amb les eines ja vistes (interfícies, genèrics, polimorfisme); la reflexió és l'eina correcta quan el tipus concret no es coneix fins a temps d'execució, no un substitut general d'un bon disseny orientat a objectes.
Exercicis
-
Fent servir
typeofiGetType(), comprova quetypeof(Revista)inew Revista("Muy Interesante", "Redaccion", 350).GetType()representen el mateixType. Imprimeix tambétipus.Nameitipus.BaseType?.Nameper aRevista(pista:BaseTyperetorna elTypede la classe base, aquíMaterialBibliotecari). -
Escriu la funció
MostrarPropietats(object obj)d'aquesta lliçó i utilitza-la per mostrar les propietats d'unLlibre, unaRevistai unSocidiferents. -
Escriu una funció
bool TePropietat(object obj, string nomPropietat)que retornitruesi el tipus d'objté una propietat pública amb aquest nom exacte (fent servirGetPropertyi comprovant si el resultat és diferent denull), ifalseen cas contrari. Prova-la amb"Titol"(ha de donartruesobre unLlibre) i amb"Preu"(ha de donarfalse).
Solucions
Type tipusPerTypeof = typeof(Revista);
Revista revista1 = new Revista("Muy Interesante", "Redaccion", 350);
Type tipusPerInstancia = revista1.GetType();
Console.WriteLine(tipusPerTypeof == tipusPerInstancia); // True
Console.WriteLine(tipusPerTypeof.Name); // "Revista"
Console.WriteLine(tipusPerTypeof.BaseType?.Name); // "MaterialBibliotecari"
static void MostrarPropietats(object obj)
{
Type tipus = obj.GetType();
Console.WriteLine($"--- {tipus.Name} ---");
foreach (PropertyInfo propietat in tipus.GetProperties())
{
Console.WriteLine($"{propietat.Name}: {propietat.GetValue(obj)}");
}
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("Muy Interesante", "Redaccion", 350);
Soci soci1 = new Soci(1, "Ana Martinez");
MostrarPropietats(llibre1);
MostrarPropietats(revista1);
MostrarPropietats(soci1);
static bool TePropietat(object obj, string nomPropietat)
{
return obj.GetType().GetProperty(nomPropietat) != null;
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(TePropietat(llibre1, "Titol")); // True
Console.WriteLine(TePropietat(llibre1, "Preu")); // False
Conclusió
En aquesta lliçó has conegut la reflexió: com obtenir el Type d'una classe amb typeof o
GetType(), com inspeccionar les seves propietats i mètodes amb GetProperties()/GetMethods(),
com llegir i escriure valors per reflexió amb GetValue()/SetValue(), i com crear instàncies
dinàmicament amb Activator.CreateInstance. També has vist que la reflexió és precisament el
mecanisme que fa possibles eines ja fetes servir al curs, com JsonSerializer o Entity Framework
Core, i per què convé reservar-la per a codi genèric d'infraestructura en lloc del dia a dia del
domini.
La lliçó següent, Atributs, retoma alguna cosa que ja has vist de passada sense aturar-t'hi:
[JsonPropertyName] i [JsonPolymorphic] de la lliçó de Serialització són, en realitat,
atributs, una manera d'anotar codi amb metadades addicionals. I la reflexió que acabes
d'aprendre és exactament l'eina que permet llegir aquests atributs en temps d'execució per
prendre decisions —la peça que faltava per entendre com funcionen de veritat per sota.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
