¿Cómo llamo a un método genérico usando una variable de tipo?

Resuelto Bevan asked hace 15 años • 9 respuestas

¿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 lo 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 avatar Oct 24 '08 12:10 Bevan
Aceptado

Debe usar la reflexión para comenzar con el método y 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, páselo nullcomo primer argumento a Invoke. Eso no tiene nada que ver con métodos genéricos, es simplemente una reflexión normal.

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

Jon Skeet avatar Oct 24 '2008 06:10 Jon Skeet

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 porque se pierde la verificación en tiempo de compilación de GenericMethod. Si luego realiza una refactorización y cambia el nombre GenericMethod, este código no se dará cuenta y fallará en 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 fallar.

Entonces, si conoce el método al que 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 en tiempo de compilación GenericMethodaquí, y si refactoriza, elimina o hace algo con GenericMethod, este código seguirá funcionando, o al menos se interrumpirá en el momento de la compilación (si, por ejemplo, elimina GenericMethod).

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

Adrian Gallero avatar Feb 27 '2011 16:02 Adrian Gallero

Llamar a un método genérico con un parámetro de tipo conocido sólo en tiempo de ejecución se puede simplificar enormemente utilizando un dynamictipo en lugar de la API de reflexión.

Para utilizar esta técnica, el tipo debe conocerse a partir del objeto real (no sólo 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, cuyo tipo en el uso "normal" se habría inferido, entonces simplemente se trata de convertir el objeto de tipo desconocido a dynamic. He aquí 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 (mediante el GetType()método) y el tipo del parámetro genérico (mediante typeofel operador).

Al convertir el argumento del objeto a dynamictipo, aplazamos el suministro del parámetro de tipo hasta el tiempo de ejecución. Cuando Processse llama al método con el dynamicargumento, al compilador no le importa el tipo de este argumento. El compilador genera código que en tiempo de ejecución verifica los tipos reales de argumentos pasados ​​(mediante la reflexión) y elige el mejor método para llamar. Aquí sólo 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 tipo dinámico es definitivamente más corta y más fácil de escribir. Tampoco deberías preocuparte 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é de DLR. Por supuesto, puedes escribir código que almacene en caché a los delegados invocados, pero al usar el dynamictipo obtienes 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), entonces puede incluir 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 dynamicun objeto como reemplazo del uso de la API de reflexión es que solo se pierde la verificación en tiempo de compilación de este tipo particular que no se conoce hasta el tiempo de ejecución. El compilador analiza estáticamente otros argumentos y el nombre del método 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 en MethodInfo.Invoke.

A continuación se muestra un ejemplo sencillo 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í ejecutamos nuevamente algún método convirtiendo el argumento al dynamictipo. Sólo la verificación del tipo del primer argumento se pospone al tiempo de ejecución. Obtendrá un error del compilador 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 se pasa el dynamicargumento a un método, esta llamada se vincula ú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 Alphala clase no implementa esta interfaz). Pero ese es el punto. El compilador no tiene información de que esta llamada sea válida. Usted, como programador, lo sabe y debe asegurarse de que este código se ejecute sin errores.

Tipo de retorno atrapado

Cuando llamas a un método no nulo con un parámetro de tipo dinámico, su tipo de retorno probablemente también lo serádynamic . Entonces, si cambiara el ejemplo anterior a este código:

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

then the type of the result object would be dynamic. This is because the compiler don't always know which method will be called. If you know the return type of the function call then you should implicitly convert it to the required type so the rest of the code is statically typed:

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

You'll get a runtime error if the type doesn't match.

Actually, if you try to get the result value in the previous example then you'll get a runtime error in the second loop iteration. This is because you tried to save the return value of a void function.

Mariusz Pawelski avatar Mar 16 '2014 19:03 Mariusz Pawelski