Ir al contenido principal

Atributos y reflexión

Los atributos y la reflexión son dos características avanzadas de C# que permiten a los programas examinar y modificar su propia estructura en tiempo de ejecución. Los atributos proporcionan una forma de añadir metadatos a los elementos del código, mientras que la reflexión permite inspeccionar tipos, miembros y obtener información sobre los objetos durante la ejecución. Estas características son fundamentales para frameworks como ASP.NET, Entity Framework y muchas librerías que requieren introspección de código.

Aunque pueden parecer conceptos complejos al principio, los atributos y la reflexión son herramientas poderosas que permiten crear aplicaciones más flexibles y dinámicas. Su comprensión es esencial para entender cómo funcionan internamente muchos frameworks modernos de .NET.

Atributos en C#

Los atributos son clases especiales que se derivan de la clase System.Attribute y se utilizan para añadir información descriptiva (metadatos) a elementos del código como clases, métodos, propiedades, parámetros, etc. Esta información puede ser utilizada posteriormente por herramientas de desarrollo, compiladores o en tiempo de ejecución mediante reflexión.

Sintaxis básica de los atributos

Los atributos se declaran entre corchetes antes del elemento al que se aplican:

[NombreAtributo]
public class MiClase
{
    [NombreAtributo]
    public void MiMetodo()
    {
        // Implementación
    }
}

Atributos predefinidos comunes

C# incluye varios atributos predefinidos que se utilizan frecuentemente:

Atributo Propósito Uso típico
[Obsolete] Marca elementos como obsoletos Deprecar métodos o clases
[Serializable] Indica que una clase puede ser serializada Serialización de objetos
[DllImport] Importa funciones de DLLs nativas Interoperabilidad con código nativo
[Conditional] Compilación condicional de métodos Debug y logging
[DebuggerDisplay] Personaliza la visualización en el depurador Facilitar debugging

Veamos ejemplos prácticos de estos atributos:

using System;
using System.Diagnostics;

// Atributo Obsolete para marcar métodos deprecados
public class CalculadoraAntiqua
{
    [Obsolete("Use CalcularNuevo en su lugar")]
    public int CalcularViejo(int a, int b)
    {
        return a + b;
    }
    
    public int CalcularNuevo(int a, int b)
    {
        return a + b;
    }
}

// Atributo DebuggerDisplay para mejorar la experiencia de debugging
[DebuggerDisplay("Nombre = {Nombre}, Edad = {Edad}")]
public class Persona
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    
    // Atributo Conditional para logging condicional
    [Conditional("DEBUG")]
    public void RegistrarAccion(string accion)
    {
        Console.WriteLine($"DEBUG: {accion} ejecutada por {Nombre}");
    }
}

// Ejemplo de uso
class Program
{
    static void Main()
    {
        var calculadora = new CalculadoraAntiqua();
        
        // Esto generará una advertencia del compilador
        int resultado = calculadora.CalcularViejo(5, 3);
        
        var persona = new Persona { Nombre = "Ana", Edad = 30 };
        persona.RegistrarAccion("Saludar"); // Solo se ejecuta en modo DEBUG
    }
}

Creación de atributos personalizados

Para crear un atributo personalizado, necesitamos crear una clase que herede de System.Attribute:

using System;

// Definición de un atributo personalizado
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public class ValidacionAttribute : Attribute
{
    public string Mensaje { get; set; }
    public bool EsRequerido { get; set; }
    
    public ValidacionAttribute(string mensaje)
    {
        Mensaje = mensaje;
        EsRequerido = true;
    }
}

// Aplicación del atributo personalizado
public class Usuario
{
    [Validacion("El nombre es obligatorio")]
    public string Nombre { get; set; }
    
    [Validacion("La edad debe ser mayor a 0", EsRequerido = false)]
    public int Edad { get; set; }
    
    [Validacion("Método de validación de email")]
    public bool ValidarEmail(string email)
    {
        return email.Contains("@");
    }
}

El atributo AttributeUsage especifica dónde puede aplicarse nuestro atributo personalizado:

Valor de AttributeTargets Descripción
Class Se puede aplicar a clases
Method Se puede aplicar a métodos
Property Se puede aplicar a propiedades
Field Se puede aplicar a campos
All Se puede aplicar a cualquier elemento

Reflexión en C#

La reflexión es la capacidad de un programa para examinar y modificar su propia estructura y comportamiento en tiempo de ejecución. Permite obtener información sobre tipos, crear instancias dinámicamente, invocar métodos y acceder a miembros.

Conceptos fundamentales de la reflexión

La reflexión se basa en varios tipos principales del espacio de nombres System.Reflection:

Tipo Propósito Uso principal
Type Representa información sobre un tipo Obtener metadatos de clases
Assembly Representa un ensamblado Cargar y examinar DLLs
MethodInfo Información sobre métodos Invocar métodos dinámicamente
PropertyInfo Información sobre propiedades Acceder a propiedades
FieldInfo Información sobre campos Acceder a campos

Obtención de información sobre tipos

using System;
using System.Reflection;

public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    
    public void MostrarInfo()
    {
        Console.WriteLine($"{Nombre}: {Precio:C}");
    }
    
    private void MetodoPrivado()
    {
        Console.WriteLine("Método privado ejecutado");
    }
}

class Program
{
    static void Main()
    {
        // Obtener información del tipo
        Type tipoProducto = typeof(Producto);
        
        Console.WriteLine($"Nombre del tipo: {tipoProducto.Name}");
        Console.WriteLine($"Espacio de nombres: {tipoProducto.Namespace}");
        Console.WriteLine($"Es clase: {tipoProducto.IsClass}");
        Console.WriteLine();
        
        // Obtener propiedades
        Console.WriteLine("Propiedades:");
        PropertyInfo[] propiedades = tipoProducto.GetProperties();
        foreach (PropertyInfo propiedad in propiedades)
        {
            Console.WriteLine($"- {propiedad.Name} ({propiedad.PropertyType.Name})");
        }
        Console.WriteLine();
        
        // Obtener métodos
        Console.WriteLine("Métodos:");
        MethodInfo[] metodos = tipoProducto.GetMethods();
        foreach (MethodInfo metodo in metodos)
        {
            if (metodo.DeclaringType == tipoProducto) // Solo métodos de esta clase
            {
                Console.WriteLine($"- {metodo.Name}");
            }
        }
    }
}

Creación dinámica de instancias

La reflexión permite crear objetos sin conocer su tipo en tiempo de compilación:

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        // Crear instancia usando Activator.CreateInstance
        Type tipoProducto = typeof(Producto);
        object instanciaProducto = Activator.CreateInstance(tipoProducto);
        
        // Establecer valores de propiedades dinámicamente
        PropertyInfo propiedadNombre = tipoProducto.GetProperty("Nombre");
        PropertyInfo propiedadPrecio = tipoProducto.GetProperty("Precio");
        PropertyInfo propiedadId = tipoProducto.GetProperty("Id");
        
        propiedadId.SetValue(instanciaProducto, 1);
        propiedadNombre.SetValue(instanciaProducto, "Laptop");
        propiedadPrecio.SetValue(instanciaProducto, 899.99m);
        
        // Obtener valores dinámicamente
        int id = (int)propiedadId.GetValue(instanciaProducto);
        string nombre = (string)propiedadNombre.GetValue(instanciaProducto);
        decimal precio = (decimal)propiedadPrecio.GetValue(instanciaProducto);
        
        Console.WriteLine($"Producto creado: ID={id}, Nombre={nombre}, Precio={precio:C}");
        
        // Invocar método dinámicamente
        MethodInfo metodoMostrar = tipoProducto.GetMethod("MostrarInfo");
        metodoMostrar.Invoke(instanciaProducto, null);
    }
}

Lectura de atributos mediante reflexión

La combinación de atributos y reflexión es especialmente poderosa:

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        Type tipoUsuario = typeof(Usuario);
        
        // Examinar atributos en propiedades
        PropertyInfo[] propiedades = tipoUsuario.GetProperties();
        foreach (PropertyInfo propiedad in propiedades)
        {
            ValidacionAttribute[] atributos = (ValidacionAttribute[])
                propiedad.GetCustomAttributes(typeof(ValidacionAttribute), false);
            
            if (atributos.Length > 0)
            {
                ValidacionAttribute validacion = atributos[0];
                Console.WriteLine($"Propiedad: {propiedad.Name}");
                Console.WriteLine($"  Mensaje: {validacion.Mensaje}");
                Console.WriteLine($"  Es requerido: {validacion.EsRequerido}");
                Console.WriteLine();
            }
        }
        
        // Examinar atributos en métodos
        MethodInfo[] metodos = tipoUsuario.GetMethods();
        foreach (MethodInfo metodo in metodos)
        {
            if (metodo.DeclaringType == tipoUsuario)
            {
                ValidacionAttribute[] atributos = (ValidacionAttribute[])
                    metodo.GetCustomAttributes(typeof(ValidacionAttribute), false);
                
                if (atributos.Length > 0)
                {
                    Console.WriteLine($"Método: {metodo.Name}");
                    Console.WriteLine($"  Mensaje: {atributos[0].Mensaje}");
                    Console.WriteLine();
                }
            }
        }
    }
}

Ejemplo práctico: Sistema de validación

Un caso de uso común es crear un sistema de validación usando atributos y reflexión:

using System;
using System.Reflection;
using System.Collections.Generic;

// Atributos de validación personalizados
public class RequeridoAttribute : Attribute
{
    public string Mensaje { get; set; } = "Este campo es requerido";
}

public class LongitudMinimaAttribute : Attribute
{
    public int Longitud { get; }
    public string Mensaje { get; set; }
    
    public LongitudMinimaAttribute(int longitud)
    {
        Longitud = longitud;
        Mensaje = $"Debe tener al menos {longitud} caracteres";
    }
}

// Clase modelo con validaciones
public class RegistroUsuario
{
    [Requerido(Mensaje = "El nombre de usuario es obligatorio")]
    [LongitudMinima(3, Mensaje = "El usuario debe tener al menos 3 caracteres")]
    public string NombreUsuario { get; set; }
    
    [Requerido(Mensaje = "El email es obligatorio")]
    public string Email { get; set; }
    
    [LongitudMinima(8, Mensaje = "La contraseña debe tener al menos 8 caracteres")]
    public string Contrasena { get; set; }
}

// Sistema de validación usando reflexión
public class Validador
{
    public static List<string> Validar(object objeto)
    {
        var errores = new List<string>();
        Type tipo = objeto.GetType();
        
        PropertyInfo[] propiedades = tipo.GetProperties();
        
        foreach (PropertyInfo propiedad in propiedades)
        {
            object valor = propiedad.GetValue(objeto);
            
            // Validar atributo Requerido
            if (propiedad.IsDefined(typeof(RequeridoAttribute)))
            {
                var atributoRequerido = propiedad.GetCustomAttribute<RequeridoAttribute>();
                
                if (valor == null || (valor is string str && string.IsNullOrWhiteSpace(str)))
                {
                    errores.Add($"{propiedad.Name}: {atributoRequerido.Mensaje}");
                }
            }
            
            // Validar atributo LongitudMinima
            if (propiedad.IsDefined(typeof(LongitudMinimaAttribute)) && valor is string textoValor)
            {
                var atributoLongitud = propiedad.GetCustomAttribute<LongitudMinimaAttribute>();
                
                if (textoValor.Length < atributoLongitud.Longitud)
                {
                    errores.Add($"{propiedad.Name}: {atributoLongitud.Mensaje}");
                }
            }
        }
        
        return errores;
    }
}

// Ejemplo de uso del sistema de validación
class Program
{
    static void Main()
    {
        var usuario = new RegistroUsuario
        {
            NombreUsuario = "ab", // Muy corto
            Email = "",           // Vacío
            Contrasena = "123"    // Muy corta
        };
        
        List<string> errores = Validador.Validar(usuario);
        
        if (errores.Count > 0)
        {
            Console.WriteLine("Errores de validación:");
            foreach (string error in errores)
            {
                Console.WriteLine($"- {error}");
            }
        }
        else
        {
            Console.WriteLine("Validación exitosa");
        }
    }
}

Consideraciones de rendimiento y buenas prácticas

La reflexión es una herramienta poderosa pero debe usarse con cuidado:

Aspecto Recomendación
Rendimiento La reflexión es más lenta que el acceso directo
Cacheo Cachea objetos Type, MethodInfo, etc. cuando sea posible
Seguridad Ten cuidado al acceder a miembros privados
Mantenimiento El código con reflexión es más difícil de mantener
Depuración Los errores en reflexión aparecen en tiempo de ejecución
// Ejemplo de cacheo para mejorar rendimiento
public static class CacheReflexion
{
    private static readonly Dictionary<Type, PropertyInfo[]> CachePropiedades 
        = new Dictionary<Type, PropertyInfo[]>();
    
    public static PropertyInfo[] ObtenerPropiedades(Type tipo)
    {
        if (!CachePropiedades.ContainsKey(tipo))
        {
            CachePropiedades[tipo] = tipo.GetProperties();
        }
        
        return CachePropiedades[tipo];
    }
}

Resumen

Los atributos y la reflexión son características avanzadas que añaden metaprogramación a C#. Los atributos permiten decorar el código con información adicional que puede ser procesada en tiempo de compilación o ejecución, mientras que la reflexión proporciona la capacidad de examinar y manipular tipos dinámicamente. Aunque estas características requieren un uso cuidadoso debido a sus implicaciones en rendimiento y complejidad, son fundamentales para entender y crear frameworks modernos, sistemas de validación, serialización y muchas otras funcionalidades avanzadas en aplicaciones .NET.