La potencia de la Reflexión en C# (Parte 4: Métodos)

JorTurFer       2 comentarios en La potencia de la Reflexión en C# (Parte 4: Métodos)
Imagen con el logo de C# para la entrada de reflexión para métodos

Aquí estamos una semana más hablando de la reflexión, y hoy les toca el turno a los métodos. Echando la vista atrás, hemos hablado sobre la reflexión en general y las propiedades, sobre como trabajar con ensamblados, y en la última semana hablamos sobre cómo utilizar los constructores mediante reflexión.

Hoy es el turno de trabajar con reflexión en métodos. A estas alturas, seguramente puedas imaginar cómo los vamos a obtener, puesto que es muy parecido a obtener los constructores. Salvo que esta vez, en vez de obtener un ConstructorInfo, vamos a tener un MethodInfo. (Aunque las dos clases en profundidad tienen sus diferencias, ambas heredan de MemberInfo). Además, lo hemos utilizado para probar el código en las entradas anteriores.

Obteniendo un método por reflexión

Como en todos los casos anteriores, lo que vamos a partir es del tipo (Type), pero esta vez vamos a llamar al método GetMethod() indicándole el nombre del método que queremos buscar. Adicionalmente, le podemos indicar también los modificadores y los parámetros, en caso de que tengamos varios métodos que se llaman igual, pero cambia la firma. Por ejemplo:

var className = "PostReflexion.ClaseEjemplo";
var assembly = Assembly.GetAssembly(typeof(Program));
var type = assembly.GetType(className);

//Obtenemos un método público
var publicMethod = type.GetMethod("Multiplicar");
//Obtenemos un método privado
var privateMethod = type.GetMethod("ResetValor", BindingFlags.Instance | BindingFlags.NonPublic);
//Obtenemos un método estático
var staticMethod = type.GetMethod("Sumar", BindingFlags.Static | BindingFlags.NonPublic);


public class ClaseEjemplo
{
    private int _valor;
    public ClaseEjemplo(int valor)
    {
        _valor = valor;
    }

    public int Multiplicar(int por)
    {
        return _valor * por;
    }

    private void ResetValor()
    {
        _valor = 0;
    }

    static int Sumar(int a, int b)
    {
        return a + b;
    }
}

Invocando nuestro método

Una vez que tenemos nuestro MethodInfo, al igual que veíamos con los constructores, basta que llamemos a Invoke pasándole los parámetros, y ya hemos conseguido ejecutar nuestro código:

var className = "PostReflexion.ClaseEjemplo";
var assembly = Assembly.GetAssembly(typeof(Program));
var type = assembly.GetType(className);

//Obtenemos los constructores
var constructorConParametros = type.GetConstructor(new[] { typeof(int) }); //Contructor con parametro int
var constructorSinParametros = type.GetConstructor(Type.EmptyTypes); //Constructor genérico

//Creamos el objeto de manera dinámica
var objetoConParametros = constructorConParametros.Invoke(new object[] { 2 });

// Creamos una referencia al método   
var m = assembly.GetType(className).GetMethod("Multiplicar");

//Llamamos al método pasandole el objeto creado dinámicamente y los argumentos dentro de un object[]
var retConstructorParamatrizado = m.Invoke(objetoConParametros, new object[] { 3 });

Con esto tan sencillo, ya hemos conseguido ejecutar métodos por reflexión en C#. ¡Y esto es todo! ¿O no…?

El coste de la reflexión

Estamos ya por la cuarta entrada, y siempre diciendo que la reflexión es cara, pero…. ¿Cuánto más cuesta procesar nuestro código por reflexión?,¿es mucho…? ¿poco…? La respuesta es: muchísimo, vamos a poner unos números:

La imagen muestra un benchmark comparando la llamada normal al método y la llamada por reflexión, viendo que esta última es más de 3500 veces más lenta

Vaya… con estos números, parece que la reflexión no sirve, es demasiado lenta, no podemos meter reflexión en nuestro código más allá de para facilitarnos las pruebas unitarias.

La reflexión es muy útil para testing, hoy sin ir más lejos la he utilizado para poder testear cierta parte del código de un proyecto que es privada porque así debe serlo, y he podido saltarme las restricciones de acceso y he podido añadir tests para ese código que prueba solo lo que tiene que probar.

Muy acertadamente en los comentarios de una entrada anterior, planteaban una alternativa para mejorar el rendimiento, que consiste crear el objeto mediante el constructor por reflexión, y a partir de ahí utilizar dynamic. Para quién no lo conozca, dynamic es un tipo especial de objeto que evita la comprobación de tipos en compilación utilizando Dynamic Language Runtime, delegando al runtime ese trabajo, lo que nos permite hacer algo como esto:

var className = "PostReflexion.ClaseEjemplo";
var assembly = Assembly.GetAssembly(typeof(Program));
var type = assembly.GetType(className);

//Obtenemos el constructor
var constructorConParametros = type.GetConstructor(new[] { typeof(int) }); 

//Creamos el objeto de manera dinámica y de tipo "dynamic"
dynamic objetoDinamicoConParametros = constructorConParametros.Invoke(new object[] { 2 });

//Llamamos al método como si fuese un objeto tipado
var retConstructorParamatrizado = objetoDinamicoConParametros.Multiplicar(3);

Con este pequeño cambio, hemos conseguido mejorar los tiempos:

La imagen muestra como utilizar "dynamic" es cerca de 23 veces más rápido que utilizar reflexión, aunque sigue siendo 150 veces más lento que una llamada normal

Los dos principales problemas de esto, son que hemos perdido la flexibilidad que teníamos para buscar los métodos, ya que ahora el nombre tiene que coincidir, no tenemos ese margen para buscar (además de que volvemos a regirnos por los modificadores de acceso), y sobre todo, sigue siendo 150 más lento que hacer una llamada convencional (aunque hemos mejorado mucho respecto a la llamada por reflexión). De todos modos, si queremos utilizar la reflexión para cargar código que puede cambiar, sigue siendo demasiado lento. Llegados a este punto, ¿tenemos que tirar la toalla con la reflexión?

Creando delegados de métodos obtenido por reflexión

Un delegado, no es más que la referencia a un método desde la cual vamos a poder llamar a ese método sin necesidad de tener acceso al objeto que lo contiene, ya que este va implícito en la referencia a donde apunta el delegado:

// Declaramos la firma del delegado
delegate void MyDelegado(string str);

// Declaramos un método con la misma firma del delegado
static void Notify(string message)
{
    Console.WriteLine(message);
}

// Creamos una instancia del delegado.
MyDelegado del1 = new MyDelegado(Notify);

// Llamamos al método a través del delegado.
del1("Llamada desde un delegado");

Ahora, si unimos esto con la reflexión, lo que podemos hacer es crear un delegado que apunte hacia nuestro método por reflexión. Para ello, siguiendo con nuestro ejemplo, vamos a utilizar el delegado «Func<int,int>» (recordemos que nuestro método recibía un entero como parámetro y devolvía otro entero), pero si fuese necesario, podemos definir el delegado que nos haga falta.

Para construirlo, vamos a aprovecharnos de Delegate.CreateDelegate, y le vamos a indicar el tipo, el objeto al que hacemos referencia, y el nombre del método al que queremos hacer el delegado:

Func<int, int> delegateMethod = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), target, "Multiplicar");

Delegate.CreateDelegate tiene diferentes parámetros en función de lo que queramos hacer, por ejemplo, si queremos crear un delegado a un método estático, tendríamos que utilizar el objeto MethodInfo directamente, te recomiendo que le eches un ojo a sus posibilidades.

Además, como conocemos el tipo del delegado a la perfección (sabemos que esperar, o podemos consultarlo sobre el MethodInfo para saber que delegado aplicarle), podemos hacer un cast al tipo de delegado concreto, por ejemplo, el código completo sería algo así:

var className = "PostReflexion.ClaseEjemplo";
var assembly = Assembly.GetAssembly(typeof(Program));
var type = assembly.GetType(className);

//Obtenemos el constructor
var constructorConParametros = type.GetConstructor(new[] { typeof(int) }); 

//Creamos el objeto de manera dinámica            
object objetoDinamicoConParametros = constructorConParametros.Invoke(new object[] { 2 });
            
//Creamos el delegado pasandole el tipo, una referencia al objeto sobre el que va a trabajar, y el nombre del metodo
//Delegate.CreateDelegate tiene diferentes parámetros en función de lo que queramos hacer
Func<int, int> delegateMethod = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), objetoDinamicoConParametros, "Multiplicar");

//Llamamos al método 
var retDelegado = delegateMethod(3);

Si ejecutas este código, veras que funciona perfectamente (Como siempre, he dejado actualizado el repositorio en GitHub para poder descargárselo y tocarlo). Además, este modo si nos permite saltarnos los limitadores de acceso, en caso de que estemos haciendo pruebas al código.

Vale, es todo muy bonito, pero a simple vista, solo supone más trabajo porque tenemos que gestionar un delegado también, ¿esto aporta algo?, vamos a ver los números:

La imagen muestra el resultado del benchmark de los 4 métodos, viendose: Llamada convencional 0.02 ns, llamada por reflexión 213.90 ns, llamada con dynamic 9.89 ns y llamada con delegado 1.74 ns

Como se puede comprobar, después de hacer una llamada convencional, utilizar delegados es lo siguiente más rápido, siendo 5,7 veces más rápido que utilizar «dynamic».

Los tiempos son solo orientativos (de hecho, han ido variando ligeramente durante los diferentes benchmark)

Conclusiones

Si bien es cierto que no podemos conseguir los tiempos que conseguiríamos con una llamada convencional. Cuando se recurre a la reflexión es precisamente cuando las llamadas convencionales no están disponibles por una razón u otra. Como se puede comprobar de los números, la reflexión es cara, pero existen maneras de hacerla más liviana.

Como decíamos en la entrada de los constructores, un gran poder conlleva una gran responsabilidad. La reflexión es algo que debemos utilizar con cabeza y criterio, pero no es algo prohibido que no hay que tocar. Como he dicho varias veces, es una herramienta especialmente útil para hacer testing por ejemplo, cuando hay cosas donde necesitamos acceder y no podemos.

2 pensamientos en “La potencia de la Reflexión en C# (Parte 4: Métodos)

Deja un comentario