La reflexión permite a un programa examinar tipos mientras se ejecuta: qué propiedades tiene una clase, cuáles son sus valores, qué métodos existen, qué atributos lleva asociados. Los atributos son la otra mitad: etiquetas declarativas entre corchetes, como [Obsolete] o [JsonPropertyName("id")], que por sí solas no significan nada hasta que el compilador o algún código las lee con reflexión. Los serializadores, los ORM, las bibliotecas de validación, los frameworks de pruebas y el enrutamiento de ASP.NET funcionan todos así.
La mayor parte del código de aplicación rara vez necesita la reflexión directamente, pero saber cómo funciona explica mucho del comportamiento de los frameworks.
Objetos Type: typeof y GetType
Todo empieza por un System.Type. Hay dos formas de obtener uno:
Salida:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
La diferencia que hay que recordar: typeof(X) nombra un tipo que conoces al escribir el código, y obj.GetType() le pregunta a un objeto qué es en realidad. Comparar GetType() == typeof(Employee) es una coincidencia exacta que falla con las clases derivadas, lo que normalmente no es lo que quieres; is e IsAssignableFrom respetan la herencia. nameof se parece pero no es reflexión en absoluto: el compilador lo sustituye por una constante string.
GetType() sobre una referencia null lanza NullReferenceException, porque no hay ningún objeto al que preguntar. Sobre un tipo de valor nullable con boxing devuelve el tipo subyacente: ((int?)5).GetType() es System.Int32.
Leer y asignar propiedades
GetProperties() enumera las propiedades públicas de un tipo como objetos PropertyInfo, cada uno de los cuales puede leer y escribir el valor en una instancia dada:
Salida:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Esto muestra tres cosas:
GetValuedevuelveobject, así que los tipos de valor vuelven con boxing y hay que hacerles un cast para usarlos.GetPropertycon un nombre que no existe devuelvenull, y la siguiente llamada sobre él lanzaNullReferenceException. Compruébalo antes de usarlo.BindingFlags.NonPublic | BindingFlags.Instancellega a los miembros privados. Es legítimo en herramientas y pruebas, pero se salta el encapsulamiento y se rompe en silencio cuando se refactoriza la clase.
Este bucle es en esencia cómo funciona un exportador a CSV o un serializador de objetos a JSON: recorrer las propiedades, leer cada valor y darle formato.
Llamar a métodos y crear objetos por su nombre
GetMethod encuentra un método, e Invoke lo llama con un array de argumentos. Activator.CreateInstance crea un objeto a partir de un Type, que es como los sistemas de plugins y los contenedores de inyección de dependencias construyen tipos elegidos en tiempo de ejecución:
Salida:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly limita la lista a los miembros declarados en la propia clase; sin él, GetMethods también devuelve ToString, Equals, GetHashCode y GetType de object. Type.GetType("Name") necesita el nombre cualificado con el namespace y, para tipos de otros ensamblados, también el nombre del ensamblado ("MyApp.Plugins.Csv, MyApp.Plugins").
Si el método invocado lanza una excepción, Invoke la envuelve en una TargetInvocationException; la original está en su InnerException.
Atributos: etiquetas que leen el compilador y los frameworks
Un atributo se escribe entre corchetes delante de lo que describe. El framework define muchos; algunos cambian lo que hace el compilador:
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
Otros los leen las bibliotecas en tiempo de ejecución: [JsonPropertyName] y [JsonIgnore] los lee System.Text.Json, [Required] y [MaxLength] la validación de modelos de ASP.NET Core y Entity Framework, [HttpGet("orders/{id}")] el enrutamiento de ASP.NET, y [Fact] y [Test] los ejecutores de pruebas. El atributo en sí no hace nada; lo hace el código que lo busca.
El nombre ObsoleteAttribute se acorta a [Obsolete] al aplicarlo: por convención, el nombre de toda clase de atributo termina en Attribute, y C# te deja omitir el sufijo.
Declarar y leer un atributo propio
Un atributo propio es una clase que deriva de Attribute. [AttributeUsage] dice a qué puede aplicarse. Los parámetros del constructor se convierten en argumentos posicionales, y las propiedades públicas asignables en argumentos con nombre:
Salida:
Username must be at most 20 characters
Email is required
Keep the city code short
0
Es una versión en miniatura de lo que hace la validación de modelos de ASP.NET Core con System.ComponentModel.DataAnnotations. Los argumentos de los atributos deben ser constantes de tiempo de compilación (números, strings, typeof(...), valores de enum o arrays de ellos), porque se guardan en los metadatos del ensamblado. GetCustomAttribute<T>() es un método de extensión de System.Reflection; también existe IsDefined(typeof(T)) cuando solo necesitas saber si un atributo está presente.
El coste de la reflexión
La reflexión cambia velocidad y seguridad por flexibilidad:
- Velocidad. Buscar un miembro por su nombre y llamarlo mediante
InvokeoGetValuees mucho más lento que una llamada directa, y hace boxing de los tipos de valor. Para un uso repetido, busca elPropertyInfoo elMethodInfouna vez y guárdalo, o conviértelo en un delegado conDelegate.CreateDelegateoMethodInfo.CreateDelegatey llama a ese. - Seguridad. Un nombre mal escrito o una firma cambiada compilan sin problema y fallan en tiempo de ejecución. Prefiere
nameof(Product.Price)al string"Price"siempre que puedas, para que los renombrados se detecten. - Trimming y AOT. Las aplicaciones recortadas y las de Native AOT eliminan los miembros que nada parece usar, y la reflexión oculta ese uso al análisis. Las bibliotecas modernas (
System.Text.Json,GeneratedRegex, logging) están pasando a los generadores de código fuente, que hacen el mismo trabajo en tiempo de compilación.
Usa la reflexión en las partes de un programa que de verdad no conocen sus tipos de antemano: plugins, herramientas genéricas, serializadores, métodos auxiliares de pruebas. Cuando los tipos se conocen, el código normal, los genéricos o las interfaces son más rápidos y el compilador los comprueba.
Errores comunes
GetType() == typeof(Base)para comprobar una clase base. Falla con los tipos derivados. UsaisoIsAssignableFrom.- No comprobar si es
null.GetProperty,GetMethodyType.GetTypedevuelvennullcuando nada coincide. - Reflexión en un bucle crítico sin caché. Guarda en caché el
MemberInfoo compila un delegado. - Capturar la excepción equivocada de
Invoke. La excepción real es laInnerExceptionde laTargetInvocationException. - Strings mágicos para los nombres de los miembros. Usa
nameof.
Preguntas frecuentes
¿Qué es la reflexión en C#?
La reflexión es la capacidad de un programa de inspeccionar tipos en tiempo de ejecución: enumerar las propiedades y los métodos de una clase, leer y asignar valores por su nombre, llamar a métodos, crear instancias y leer atributos. Vive en System.Reflection y parte de un objeto Type. Los serializadores, los ORM, los contenedores de inyección de dependencias y los frameworks de pruebas se construyen sobre ella.
¿Qué diferencia hay entre typeof y GetType en C#?
typeof(Customer) se resuelve en tiempo de compilación a partir del nombre de un tipo y no necesita ningún objeto. obj.GetType() se llama sobre una instancia en tiempo de ejecución y devuelve el tipo real del objeto, que puede ser más derivado que el tipo declarado de la variable: con Animal a = new Dog();, a.GetType() es Dog. GetType() sobre una referencia null lanza NullReferenceException.
¿Cómo obtengo el valor de una propiedad por su nombre en C#?
obj.GetType().GetProperty("Price") devuelve un PropertyInfo (o null si no existe esa propiedad pública), y .GetValue(obj) la lee como un object. .SetValue(obj, value) la escribe. Guarda en caché el PropertyInfo si lo haces en un bucle, porque la búsqueda es la parte cara.
¿Cómo creo un atributo personalizado en C#?
Declara una clase que derive de System.Attribute, ponle el sufijo Attribute e indica dónde puede usarse con [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Aplícalo como [MaxLength(50)] y léelo con property.GetCustomAttribute<MaxLengthAttribute>().
¿Qué hace el atributo Obsolete en C#?
[Obsolete("Use PlaceOrderAsync instead")] sobre un miembro hace que el compilador emita el aviso CS0618, con tu mensaje, en cada punto de llamada. [Obsolete("...", true)] convierte el aviso en el error CS0619. Así es como las bibliotecas retiran una API sin romper de un día para otro a quien la usa.
¿Es lenta la reflexión en C#?
Comparada con una llamada directa, sí: buscar un miembro por su nombre e invocarlo mediante MethodInfo.Invoke o PropertyInfo.GetValue suele ser de decenas a cientos de veces más lento, y hace boxing de los tipos de valor. Está bien para el arranque, la configuración y el uso ocasional. En los caminos críticos, guarda en caché el MemberInfo, construye un delegado una vez o usa en su lugar genéricos o un generador de código fuente.