1301 votes

Comment utiliser la réflexion pour appeler une méthode générique ?

Quelle est la meilleure façon d'appeler une méthode générique lorsque le paramètre de type n'est pas connu au moment de la compilation, mais est obtenu dynamiquement au moment de l'exécution ?

Considérez l'exemple de code suivant : dans la méthode Example(), quelle est la manière la plus concise d'invoquer GenericMethod() en utilisant le type stocké dans la variable myType ?

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>()
    {   
        ...
    }    
}

1362voto

Jon Skeet Points 692016

Vous devez utiliser la réflexion pour obtenir la méthode de départ, puis la "construire" en fournissant des arguments de type avec la commande MakeGenericMethod :

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

EDIT : Pour une méthode statique, passez null comme premier argument de Invoke . Cela n'a rien à voir avec les méthodes génériques - il s'agit simplement d'une réflexion normale.

195voto

Adrian Gallero Points 439

Juste un ajout à la réponse originale. Bien que cela fonctionne :

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

C'est aussi un peu dangereux dans la mesure où vous perdez la vérification à la compilation de GenericMethod. Si vous effectuez plus tard un remaniement et renommez GenericMethod, ce code ne le remarquera pas et échouera au moment de l'exécution. De plus, s'il y a un post-traitement de l'assemblage (par exemple l'obscurcissement ou la suppression des méthodes/classes inutilisées), ce code peut également échouer.

Donc, si vous connaissez la méthode à laquelle vous vous liez au moment de la compilation, et que cette méthode n'est pas appelée des millions de fois, donc l'overhead n'a pas d'importance, je changerais ce code pour qu'il soit :

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);

Bien que ce ne soit pas très joli, vous avez ici une référence au temps de compilation à GenericMethod, et si vous remaniez, supprimez ou faites quoi que ce soit avec GenericMethod, ce code continuera de fonctionner, ou au moins se cassera au temps de compilation (si par exemple vous supprimez GenericMethod).

Une autre façon de faire la même chose serait de créer une nouvelle classe wrapper, et de la créer via Activator. Je ne sais pas s'il existe un meilleur moyen.

183voto

Mariusz Pawelski Points 857

L'appel d'une méthode générique avec un paramètre de type connu uniquement au moment de l'exécution peut être grandement simplifié en utilisant dynamic au lieu de l'API de réflexion.

Pour utiliser cette technique, le type doit être connu de l'objet réel (pas seulement une instance de Type ). Sinon, vous devez créer un objet de ce type ou utiliser l'API de réflexion standard. solution . Vous pouvez créer un objet en utilisant Activator.CreateInstance méthode.

Si vous voulez appeler une méthode générique dont le type aurait été déduit dans une utilisation "normale", il suffit de couler un objet de type inconnu dans la catégorie dynamic . Voici un exemple :

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)"
        }
    }
}

et voici la sortie de ce programme :

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

Process est une méthode d'instance générique qui écrit le type réel de l'argument passé (en utilisant la fonction GetType() ) et le type de paramètre générique (en utilisant la méthode typeof opérateur).

En transformant l'argument de l'objet en dynamic nous avons reporté la fourniture du paramètre de type jusqu'au moment de l'exécution. Lorsque le Process est appelée avec dynamic alors le compilateur ne se soucie pas du type de cet argument. Le compilateur génère du code qui, à l'exécution, vérifie les types réels des arguments passés (en utilisant la réflexion) et choisit la meilleure méthode à appeler. Ici, il n'y a que cette seule méthode générique, elle est donc invoquée avec un paramètre de type approprié.

Dans cet exemple, le résultat est le même que si vous aviez écrit :

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

La version avec le type dynamique est définitivement plus courte et plus facile à écrire. Vous ne devez pas non plus vous inquiéter des performances de l'appel multiple de cette fonction. L'appel suivant avec des arguments du même type devrait être plus rapide grâce à la fonction mise en cache dans le DLR. Bien sûr, vous pouvez écrire du code qui met en cache les délégués invoqués, mais en utilisant le mécanisme de la dynamic type vous obtenez ce comportement gratuitement.

Si la méthode générique que vous voulez appeler n'a pas d'argument de type paramétré (donc son paramètre de type ne peut pas être déduit). Alors vous pouvez envelopper l'invocation de la méthode générique dans une méthode d'aide comme dans l'exemple suivant :

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) + ">");
    }
}

Sécurité accrue du type

Ce qui est vraiment génial dans l'utilisation dynamic en remplacement de l'utilisation de l'API de réflexion est que vous ne perdez que la vérification au moment de la compilation de ce type particulier que vous ne connaissez pas jusqu'au moment de l'exécution. Les autres arguments et le nom de la méthode sont analysés statiquement par le compilateur comme d'habitude. Si vous supprimez ou ajoutez des arguments, changez leur type ou renommez le nom de la méthode, vous obtiendrez une erreur de compilation. Cela ne se produira pas si vous fournissez le nom de la méthode en tant que chaîne dans le champ Type.GetMethod et les arguments sous forme de tableau d'objets dans MethodInfo.Invoke .

Voici un exemple simple qui illustre comment certaines erreurs peuvent être détectées au moment de la compilation (code commenté) et d'autres au moment de l'exécution. Il montre également comment DLR tente de résoudre la question de la méthode à appeler.

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

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);
    }
}

Ici, nous exécutons de nouveau une méthode en coulant l'argument en dynamic type. Seule la vérification du type du premier argument est reportée au moment de l'exécution. Vous obtiendrez une erreur de compilation si le nom de la méthode que vous appelez n'existe pas ou si les autres arguments sont invalides (nombre d'arguments ou types incorrects).

Quand vous passez dynamic à la méthode, alors cet appel est dernièrement lié . La résolution des surcharges des méthodes se fait au moment de l'exécution et tente de choisir la meilleure surcharge. Ainsi, si vous invoquez ProcessItem avec un objet de BarItem alors vous ferez appel à une méthode non générique car elle correspond mieux à ce type. Cependant, vous obtiendrez une erreur d'exécution si vous passez un argument de type Alpha parce qu'il n'y a aucune méthode qui peut gérer cet objet. Mais c'est là tout le problème. Le compilateur n'a pas l'information que cet appel est valide. En tant que programmeur, vous le savez et vous devez vous assurer que ce code s'exécute sans erreur.

Type de retour (gotcha)

Lorsque vous appelez une méthode non-vide avec un paramètre de type dynamique, son type de retour sera probablement être dynamic trop . Donc si vous changez l'exemple précédent par ce code :

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

alors le type d'objet résultat serait dynamic . Ceci est dû au fait que le compilateur ne sait pas toujours quelle méthode sera appelée. Si vous connaissez le type de retour de l'appel de la fonction, vous devez alors convertir implicitement en type requis pour que le reste du code soit typé statique :

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

Vous obtiendrez une erreur d'exécution si le type ne correspond pas.

En fait, si vous essayez d'obtenir la valeur du résultat dans l'exemple précédent, vous obtiendrez une erreur d'exécution à la deuxième itération de la boucle. Ceci est dû au fait que vous avez essayé de sauvegarder la valeur de retour d'une fonction void.

19voto

jbtule Points 11159

Avec C# 4.0, la réflexion n'est pas nécessaire car le DLR peut l'appeler en utilisant des types d'exécution. Étant donné que l'utilisation de la bibliothèque DLR est plutôt pénible de manière dynamique (au lieu que le compilateur C# génère du code pour vous), le framework open source Dynamitey (PCL) vous permet d'accéder facilement en cache, au moment de l'exécution, aux mêmes appels que ceux que le compilateur générerait pour vous.

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

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

6voto

Aaron Powell Points 15598

Cela correspond à une question que j'ai posée l'autre semaine : Réflexion et types génériques

J'ai ensuite couvert comment appeler une méthode générique surchargée sur mon blog : http://www.aaron-powell.com/reflection-and-generics

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X