¿Cómo uso la reflexión para llamar a un método genérico?

1071

¿Cuál es la mejor manera de llamar a un método genérico cuando el parámetro de tipo no se conoce en tiempo de compilación, sino que se obtiene dinámicamente en tiempo de ejecución?

Considere el siguiente código de muestra: dentro del Example()método, ¿cuál es la forma más concisa de invocar GenericMethod<T>()usando el Typealmacenado en la myTypevariable?

public class Sample
{
    public void Example(string typeName)
    {
        Type myType = FindType(typeName);

        // What goes here to call GenericMethod<T>()?
        GenericMethod<myType>(); // This doesn't work

        // What changes to call StaticMethod<T>()?
        Sample.StaticMethod<myType>(); // This also doesn't work
    }

    public void GenericMethod<T>()
    {
        // ...
    }

    public static void StaticMethod<T>()
    {
        //...
    }
}
Bevan
fuente
77
Probé la solución de Jon y no pude hacer que funcionara hasta que hice público el método genérico en mi clase. Sé que otro Jon respondió diciendo que necesita especificar las banderas de enlace, pero esto no ayudó.
Naskew
12
También necesita BindingFlags.Instance, no solo BindingFlags.NonPublic, obtener el método privado / interno.
Lars Kemmann
2
Versión moderna de esta pregunta: stackoverflow.com/q/2433436/103167
Ben Voigt
@ Peter Mortensen - para tu información, utilicé espacios antes del '?' para separar las partes en inglés de las partes que no están en inglés (C #); En mi humilde opinión eliminar el espacio hace que se vea como el? Es parte del código. Si no hubiera código, ciertamente estaría de acuerdo con eliminar los espacios, pero en este caso ...
Bevan

Respuestas:

1139

Debe usar la reflexión para comenzar con el método, luego "construirlo" proporcionando argumentos de tipo con MakeGenericMethod :

MethodInfo method = typeof(Sample).GetMethod(nameof(Sample.GenericMethod));
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Para un método estático, pase nullcomo primer argumento a Invoke. Eso no tiene nada que ver con los métodos genéricos: es solo una reflexión normal.

Como se señaló, mucho de esto es más simple a partir del uso de C # 4 dynamic, por supuesto, si puede usar la inferencia de tipos. No ayuda en casos donde la inferencia de tipos no está disponible, como el ejemplo exacto en la pregunta.

Jon Skeet
fuente
92
+1; tenga en cuenta que GetMethod()solo considera los métodos de instancia pública de forma predeterminada, por lo que puede necesitar BindingFlags.Staticy / o BindingFlags.NonPublic.
20
La combinación correcta de banderas es BindingFlags.NonPublic | BindingFlags.Instance(y opcionalmente BindingFlags.Static).
Lars Kemmann
44
Una pregunta que se hace dupe de esto se pregunta cómo hacer esto con métodos estáticos, y técnicamente también lo hace la pregunta aquí. El primer parámetro de generic.Invoke () debe ser nulo al llamar a métodos estáticos. El primer parámetro solo es necesario cuando se llaman métodos de instancia.
Chris Moschini
2
@ChrisMoschini: Agregó eso a la respuesta.
Jon Skeet
2
@gzou: He agregado algo a la respuesta, pero tenga en cuenta que para llamar a los métodos genéricos en la pregunta , dynamicno ayuda porque la inferencia de tipos no está disponible. (No hay argumentos que el compilador pueda usar para determinar el tipo de argumento.)
Jon Skeet
170

Solo una adición a la respuesta original. Si bien esto funcionará:

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

También es un poco peligroso ya que pierde la verificación en tiempo de compilación GenericMethod. Si luego realiza una refactorización y cambia el nombreGenericMethod , este código no se dará cuenta y fallará en el tiempo de ejecución. Además, si hay algún procesamiento posterior del ensamblaje (por ejemplo, ofuscar o eliminar métodos / clases no utilizados), este código también podría romperse.

Entonces, si conoce el método al que se está vinculando en el momento de la compilación, y esto no se llama millones de veces, por lo que la sobrecarga no importa, cambiaría este código para que sea:

Action<> GenMethod = GenericMethod<int>;  //change int by any base type 
                                          //accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Si bien no es muy bonito, tiene una referencia de tiempo de compilación GenericMethodaquí, y si refactoriza, elimina o hace algo GenericMethod, este código seguirá funcionando, o al menos se romperá en el momento de la compilación (si, por ejemplo, lo elimina GenericMethod).

Otra forma de hacer lo mismo sería crear una nueva clase de contenedor y crearla Activator. No sé si hay una mejor manera.

Adrián Gallero
fuente
55
En los casos en que se usa la reflexión para llamar a un método, es habitual que el nombre del método sea descubierto por otro método. Conocer el nombre del método de antemano no es común.
Bevan
13
Bueno, estoy de acuerdo con los usos comunes de la reflexión. Pero la pregunta original era cómo llamar a "GenericMethod <myType> ()" Si se permitiera esa sintaxis, no necesitaríamos GetMethod () en absoluto. Pero para la pregunta "¿cómo escribo" GenericMethod <myType> "? Creo que la respuesta debería incluir una forma de evitar perder el enlace en tiempo de compilación con GenericMethod. Ahora si esta pregunta es común o no, no lo sé, pero Sí sé que tuve este problema exacto ayer, y por eso llegué a esta pregunta.
Adrian Gallero
20
Podrías hacer en GenMethod.Method.GetGenericMethodDefinition()lugar de this.GetType().GetMethod(GenMethod.Method.Name). Es un poco más limpio y probablemente más seguro.
Daniel Cassidy
¿Qué significa "myType" en su muestra?
Desarrollador
37
Ahora puedes usarnameof(GenericMethod)
dmigo
140

Llamar a un método genérico con un parámetro de tipo conocido solo en tiempo de ejecución puede simplificarse enormemente usando un dynamictipo en lugar de la API de reflexión.

Para usar esta técnica, el tipo debe conocerse desde el objeto real (no solo una instancia de la Typeclase). De lo contrario, debe crear un objeto de ese tipo o utilizar la solución API de reflexión estándar . Puede crear un objeto utilizando el método Activator.CreateInstance .

Si desea llamar a un método genérico, en el uso "normal" se habría inferido su tipo, entonces simplemente se trata de enviar el objeto de tipo desconocido dynamic. Aquí hay un ejemplo:

class Alpha { }
class Beta { }
class Service
{
    public void Process<T>(T item)
    {
        Console.WriteLine("item.GetType(): " + item.GetType()
                          + "\ttypeof(T): " + typeof(T));
    }
}

class Program
{
    static void Main(string[] args)
    {
        var a = new Alpha();
        var b = new Beta();

        var service = new Service();
        service.Process(a); // Same as "service.Process<Alpha>(a)"
        service.Process(b); // Same as "service.Process<Beta>(b)"

        var objects = new object[] { a, b };
        foreach (var o in objects)
        {
            service.Process(o); // Same as "service.Process<object>(o)"
        }
        foreach (var o in objects)
        {
            dynamic dynObj = o;
            service.Process(dynObj); // Or write "service.Process((dynamic)o)"
        }
    }
}

Y aquí está el resultado de este programa:

item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta
item.GetType(): Alpha    typeof(T): System.Object
item.GetType(): Beta     typeof(T): System.Object
item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta

Processes un método de instancia genérico que escribe el tipo real del argumento pasado (usando el GetType()método) y el tipo del parámetro genérico (usando el typeofoperador).

Al convertir el argumento del objeto a dynamictipo, diferimos proporcionando el parámetro de tipo hasta el tiempo de ejecución. Cuando Processse llama al método con eldynamic argumento, al compilador no le importa el tipo de este argumento. El compilador genera código que en tiempo de ejecución comprueba los tipos reales de argumentos pasados ​​(mediante el uso de la reflexión) y elige el mejor método para llamar. Aquí solo existe este método genérico, por lo que se invoca con un parámetro de tipo adecuado.

En este ejemplo, el resultado es el mismo que si escribiera:

foreach (var o in objects)
{
    MethodInfo method = typeof(Service).GetMethod("Process");
    MethodInfo generic = method.MakeGenericMethod(o.GetType());
    generic.Invoke(service, new object[] { o });
}

La versión con un tipo dinámico es definitivamente más corta y más fácil de escribir. Tampoco debe preocuparse por el rendimiento de llamar a esta función varias veces. La próxima llamada con argumentos del mismo tipo debería ser más rápida gracias al mecanismo de almacenamiento en caché en DLR. Por supuesto, puede escribir código que cachee a los delegados invocados, pero al usar el dynamictipo obtendrá este comportamiento de forma gratuita.

Si el método genérico al que desea llamar no tiene un argumento de tipo parametrizado (por lo que no se puede inferir su parámetro de tipo), puede ajustar la invocación del método genérico en un método auxiliar como en el siguiente ejemplo:

class Program
{
    static void Main(string[] args)
    {
        object obj = new Alpha();

        Helper((dynamic)obj);
    }

    public static void Helper<T>(T obj)
    {
        GenericMethod<T>();
    }

    public static void GenericMethod<T>()
    {
        Console.WriteLine("GenericMethod<" + typeof(T) + ">");
    }
}

Mayor seguridad de tipo

Lo realmente bueno de usar un dynamicobjeto como reemplazo para usar la API de reflexión es que solo pierde el tiempo de compilación de este tipo en particular que no conoce hasta el tiempo de ejecución. Otros argumentos y el nombre del método son analizados estáticamente por el compilador como de costumbre. Si elimina o agrega más argumentos, cambia sus tipos o cambia el nombre del método, obtendrá un error en tiempo de compilación. Esto no sucederá si proporciona el nombre del método como una cadena Type.GetMethody los argumentos como la matriz de objetos MethodInfo.Invoke.

A continuación se muestra un ejemplo simple que ilustra cómo se pueden detectar algunos errores en tiempo de compilación (código comentado) y otros en tiempo de ejecución. También muestra cómo el DLR intenta resolver qué método llamar.

interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }

class Program
{
    static void Main(string[] args)
    {
        var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
        for (int i = 0; i < objects.Length; i++)
        {
            ProcessItem((dynamic)objects[i], "test" + i, i);

            //ProcesItm((dynamic)objects[i], "test" + i, i);
            //compiler error: The name 'ProcesItm' does not
            //exist in the current context

            //ProcessItem((dynamic)objects[i], "test" + i);
            //error: No overload for method 'ProcessItem' takes 2 arguments
        }
    }

    static string ProcessItem<T>(T item, string text, int number)
        where T : IItem
    {
        Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
                          typeof(T), text, number);
        return "OK";
    }
    static void ProcessItem(BarItem item, string text, int number)
    {
        Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
    }
}

Aquí nuevamente ejecutamos algún método lanzando el argumento al dynamictipo. Solo la verificación del tipo del primer argumento se pospone al tiempo de ejecución. Obtendrá un error de compilación si el nombre del método al que está llamando no existe o si otros argumentos no son válidos (número incorrecto de argumentos o tipos incorrectos).

Cuando pasa el dynamicargumento a un método, esta llamada está vinculada últimamente . La resolución de sobrecarga del método ocurre en tiempo de ejecución e intenta elegir la mejor sobrecarga. Entonces, si invoca el ProcessItemmétodo con un objeto de BarItemtipo, en realidad llamará al método no genérico, porque es una mejor coincidencia para este tipo. Sin embargo, obtendrá un error de tiempo de ejecución cuando pase un argumento del Alphatipo porque no hay ningún método que pueda manejar este objeto (un método genérico tiene la restricción where T : IItemy la Alphaclase no implementa esta interfaz). Pero ese es todo el punto. El compilador no tiene información de que esta llamada sea válida. Usted, como programador, sabe esto y debe asegurarse de que este código se ejecute sin errores.

Tipo de retorno gotcha

Cuando se llama a un método no vacío con un parámetro de tipo dinámico, su tipo de retorno probablemente también lo seadynamic . Entonces, si cambiara el ejemplo anterior a este código:

var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

entonces el tipo del objeto resultante sería dynamic. Esto se debe a que el compilador no siempre sabe qué método se llamará. Si conoce el tipo de retorno de la llamada a la función, debe convertirlo implícitamente al tipo requerido para que el resto del código se escriba estáticamente:

string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

Obtendrá un error de tiempo de ejecución si el tipo no coincide.

En realidad, si intenta obtener el valor del resultado en el ejemplo anterior, obtendrá un error de tiempo de ejecución en la segunda iteración del bucle. Esto se debe a que intentó guardar el valor de retorno de una función anulada.

Mariusz Pawelski
fuente
Mariusz, confundido por "Sin embargo, obtendrá un error de tiempo de ejecución cuando pase un argumento de tipo Alpha porque no hay ningún método que pueda manejar este objeto". Si llamo var a = new Alpha () ProcessItem (a, "test" + i , i) ¿Por qué el método genérico ProcessItem no manejaría esto de manera efectiva, generando "Elemento de proceso general"?
Alex Edelstein
@AlexEdelstein Edité mi respuesta para aclarar un poco. Es porque el ProcessItemmétodo genérico tiene una restricción genérica y acepta solo objetos que implementan la IIteminterfaz. Cuándo llamarás ProcessItem(new Aplha(), "test" , 1);o ProcessItem((object)(new Aplha()), "test" , 1);recibirás un error del compilador, pero al enviarlo dynamicpospones esa verificación en tiempo de ejecución.
Mariusz Pawelski el
Gran respuesta y explicación, funciona perfectamente para mí. Mucho mejor que la respuesta aceptada, más corta de escribir, más eficaz y más segura.
ygoe
17

Con C # 4.0, la reflexión no es necesaria ya que el DLR puede llamarla usando tipos de tiempo de ejecución. Dado que el uso de la biblioteca DLR es una molestia dinámica (en lugar de que el compilador de C # genere código para usted), el marco de código abierto Dynamitey (.net estándar 1.5) le brinda un fácil acceso en tiempo de ejecución en caché a las mismas llamadas que generaría el compilador para ti.

var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));
jbtule
fuente
13

Agregando a la respuesta de Adrián Gallero :

Llamar a un método genérico desde la información de tipo implica tres pasos.

TLDR: la llamada a un método genérico conocido con un objeto tipo se puede lograr mediante:

((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

donde GenericMethod<object>es el nombre del método para llamar y cualquier tipo que satisfaga las restricciones genéricas.

(Acción) coincide con la firma del método a llamar, es decir ( Func<string,string,int>o Action<bool>)

El paso 1 es obtener el MethodInfo para la definición del método genérico

Método 1: Use GetMethod () o GetMethods () con los tipos apropiados o banderas de enlace.

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

Método 2: Cree un delegado, obtenga el objeto MethodInfo y luego llame a GetGenericMethodDefinition

Desde dentro de la clase que contiene los métodos:

MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

Desde fuera de la clase que contiene los métodos:

MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)Sample.StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

En C #, el nombre de un método, es decir, "ToString" o "GenericMethod" en realidad se refiere a un grupo de métodos que pueden contener uno o más métodos. Hasta que proporcione los tipos de los parámetros del método, no se sabe a qué método se refiere.

((Action)GenericMethod<object>)se refiere al delegado para un método específico. ((Func<string, int>)GenericMethod<object>) se refiere a una sobrecarga diferente de GenericMethod

Método 3: Cree una expresión lambda que contenga una expresión de llamada a método, obtenga el objeto MethodInfo y luego GetGenericMethodDefinition

MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
    (Sample v) => v.GenericMethod<object>()
    )).Body).Method.GetGenericMethodDefinition();

Esto se descompone en

Cree una expresión lambda donde el cuerpo sea una llamada a su método deseado.

Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();

Extraer el cuerpo y emitir a MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Obtenga la definición del método genérico del método

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

El paso 2 está llamando a MakeGenericMethod para crear un método genérico con los tipos apropiados.

MethodInfo generic = method.MakeGenericMethod(myType);

El paso 3 invoca el método con los argumentos apropiados.

generic.Invoke(this, null);
Grax32
fuente
8

Nadie proporcionó la solución de " reflexión clásica ", así que aquí hay un ejemplo de código completo:

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

namespace DictionaryRuntime
{
    public class DynamicDictionaryFactory
    {
        /// <summary>
        /// Factory to create dynamically a generic Dictionary.
        /// </summary>
        public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
        {
            //Creating the Dictionary.
            Type typeDict = typeof(Dictionary<,>);

            //Creating KeyValue Type for Dictionary.
            Type[] typeArgs = { keyType, valueType };

            //Passing the Type and create Dictionary Type.
            Type genericType = typeDict.MakeGenericType(typeArgs);

            //Creating Instance for Dictionary<K,T>.
            IDictionary d = Activator.CreateInstance(genericType) as IDictionary;

            return d;

        }
    }
}

La DynamicDictionaryFactoryclase anterior tiene un método

CreateDynamicGenericInstance(Type keyType, Type valueType)

y crea y devuelve una instancia IDictionary, cuyos tipos de claves y valores son exactamente los especificados en la llamada keyTypey valueType.

Aquí hay un ejemplo completo de cómo llamar a este método para crear instancias y usar un Dictionary<String, int>:

using System;
using System.Collections.Generic;

namespace DynamicDictionary
{
    class Test
    {
        static void Main(string[] args)
        {
            var factory = new DictionaryRuntime.DynamicDictionaryFactory();
            var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));

            var typedDict = dict as Dictionary<String, int>;

            if (typedDict != null)
            {
                Console.WriteLine("Dictionary<String, int>");

                typedDict.Add("One", 1);
                typedDict.Add("Two", 2);
                typedDict.Add("Three", 3);

                foreach(var kvp in typedDict)
                {
                    Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
                }
            }
            else
                Console.WriteLine("null");
        }
    }
}

Cuando se ejecuta la aplicación de consola anterior, obtenemos el resultado correcto esperado:

Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3
Dimitre Novatchev
fuente
2

Estos son mis 2 centavos basados ​​en la respuesta de Grax , pero con dos parámetros necesarios para un método genérico.

Suponga que su método se define de la siguiente manera en una clase Helpers:

public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

En mi caso, el tipo U es siempre una colección observable que almacena objetos del tipo T.

Como tengo mis tipos predefinidos, primero creo los objetos "ficticios" que representan la colección observable (U) y el objeto almacenado en ella (T) y que se usarán a continuación para obtener su tipo al llamar al Make

object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);

Luego llame al GetMethod para encontrar su función genérica:

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

Hasta ahora, la llamada anterior es bastante idéntica a lo que se explicó anteriormente, pero con una pequeña diferencia cuando necesita pasarle múltiples parámetros.

Debe pasar una matriz Tipo [] a la función MakeGenericMethod que contiene los tipos de objetos "ficticios" que se crearon anteriormente:

MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

Una vez hecho esto, debe llamar al método Invoke como se mencionó anteriormente.

generic.Invoke(null, new object[] { csvData });

Y tu estas listo. Funciona un encanto!

ACTUALIZAR:

Como resaltó @Bevan, no necesito crear una matriz cuando llamo a la función MakeGenericMethod, ya que toma parámetros y no necesito crear un objeto para obtener los tipos, ya que puedo pasar los tipos directamente a esta función. En mi caso, dado que tengo los tipos predefinidos en otra clase, simplemente cambié mi código a:

object myCollection = null;

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

MethodInfo generic = method.MakeGenericMethod(
   myClassInfo.CollectionType,
   myClassInfo.ObjectType
);

myCollection = generic.Invoke(null, new object[] { csvData });

myClassInfo contiene 2 propiedades de tipo Typeque configuré en tiempo de ejecución en función de un valor de enumeración pasado al constructor y me proporcionará los tipos relevantes que luego usaré en MakeGenericMethod.

Gracias de nuevo por destacar este @Bevan.

Thierry
fuente
Los argumentos para MakeGenericMethod()tener la palabra clave params para que no necesite crear una matriz; ni necesita crear instancias para obtener los tipos, methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject))sería suficiente.
Bevan
0

Inspirado por la respuesta de Enigmativity : supongamos que tiene dos (o más) clases, como

public class Bar { }
public class Square { }

y desea llamar al método Foo<T>con Bary Square, que se declara como

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

Luego puede implementar un método de extensión como:

public static class Extension
{
    public static void InvokeFoo<T>(this T t)
    {
        var fooMethod = typeof(myClass).GetMethod("Foo");
        var tType = typeof(T);
        var fooTMethod = fooMethod.MakeGenericMethod(new[] { tType });
        fooTMethod.Invoke(new myClass(), new object[] { t });
    }
}

Con esto, simplemente puede invocar Foocomo:

var objSquare = new Square();
objSquare.InvokeFoo();

var objBar = new Bar();
objBar.InvokeFoo();

que funciona para cada clase En este caso, generará:


Barra cuadrada

Mate
fuente