web-dev-qa-db-fra.com

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

Quel est le meilleur moyen d'appeler une méthode générique lorsque le paramètre type n'est pas connu au moment de la compilation, mais qu'il est obtenu dynamiquement au moment de l'exécution?

Prenons l'exemple de code suivant: dans la méthode Example(), quel est le moyen le plus concis d'appeler GenericMethod<T>() à l'aide de la variable Type stockée 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>()
    {
        //...
    }
}
981
Bevan

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

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

Pour une méthode statique, transmettez null comme premier argument à Invoke. Cela n'a rien à voir avec les méthodes génériques - c'est juste une réflexion normale.

Comme indiqué plus haut, beaucoup de choses sont plus simples à partir de C # 4 avec dynamic - si vous pouvez utiliser l’inférence de type, bien sûr. Cela n'aide pas dans les cas où l'inférence de type n'est pas disponible, comme dans l'exemple exact de la question.

1064
Jon Skeet

Juste un ajout à la réponse originale. Alors que cela fonctionnera:

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

C’est aussi un peu dangereux en ce que vous perdez la vérification du temps de compilation pour GenericMethod. Si vous effectuez ultérieurement un refactoring et renommez GenericMethod, ce code ne le remarquera pas et échouera au moment de l'exécution. De plus, s'il existe un post-traitement de l'assembly (par exemple, masquer ou supprimer des méthodes/classes inutilisées), ce code peut également être rompu.

Donc, si vous connaissez la méthode à laquelle vous vous connectez au moment de la compilation, et que cela ne s'appelle pas des millions de fois, que la charge de traitement soit sans importance, je changerais ce code en:

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 pas très joli, vous avez une référence de compilation à GenericMethod ici, et si vous refactorisez, supprimez ou faites quoi que ce soit avec GenericMethod, ce code continuera à fonctionner, ou du moins à se rompre au moment de la 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 y a un meilleur moyen.

158
Adrian Gallero

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 un type dynamic à la place de l'API de réflexion.

Pour utiliser cette technique, le type doit être connu à partir de l'objet réel (pas seulement une instance de la classe 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 la méthode Activator.CreateInstance .

Si vous souhaitez appeler une méthode générique, son type aurait été inféré dans une utilisation "normale", puis il s'agira simplement de convertir l'objet de type inconnu en 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 le résultat 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é (à l'aide de la méthode GetType()) et le type du paramètre générique (à l'aide de l'opérateur typeof).

En transformant l'argument d'objet en type dynamic, nous avons différé la fourniture du paramètre type jusqu'à l'exécution. Lorsque la méthode Process est appelée avec l'argument dynamic, le compilateur se fiche du type de cet argument. Le compilateur génère un code qui, au moment de l'exécution, vérifie les types réels d'arguments passés (à l'aide de la réflexion) et choisit la meilleure méthode à appeler. Ici, il n'y a qu'une 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 un type dynamique est nettement plus courte et plus facile à écrire. Ne vous inquiétez pas non plus des performances liées à l’appel de cette fonction plusieurs fois. Le prochain appel avec des arguments du même type devrait être plus rapide grâce au mécanisme mise en cache du DLR. Bien sûr, vous pouvez écrire du code qui cache les délégués invoqués, mais en utilisant le type dynamic, vous obtenez ce comportement gratuitement.

Si la méthode générique que vous souhaitez appeler ne possède pas d'argument de type paramétré (son paramètre de type ne peut donc pas être inféré), vous pouvez alors envelopper l'appel de la méthode générique dans une méthode d'assistance, 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) + ">");
    }
}

Augmentation de la sécurité de type

L’utilisation de l’objet dynamic en remplacement de l’API de réflexion présente un avantage considérable: elle ne perd que la vérification du temps de compilation de ce type particulier que vous ne connaissez pas jusqu’à l’exécution. Les autres arguments et le nom de la méthode sont analysés de manière statique par le compilateur, comme d’habitude. Si vous supprimez ou ajoutez d'autres arguments, modifiez leurs types ou renommez le nom de la méthode, vous obtiendrez une erreur lors de la compilation. Cela ne se produira pas si vous fournissez le nom de la méthode sous forme de chaîne dans Type.GetMethod et d'arguments dans le tableau d'objets dans MethodInfo.Invoke.

Vous trouverez ci-dessous un exemple simple illustrant comment certaines erreurs peuvent être interceptées lors de la compilation (code commenté) et d’autres au moment de l’exécution. Il montre également comment le DLR tente de résoudre la méthode à appeler.

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

Ici, nous exécutons encore une méthode en convertissant l'argument dans le type dynamic. Seule la vérification du type du premier argument est reportée à l'exécution. Vous obtiendrez une erreur de compilation si le nom de la méthode que vous appelez n'existe pas ou si d'autres arguments ne sont pas valides (nombre d'arguments incorrect ou types incorrects).

Lorsque vous passez l'argument dynamic à une méthode, cet appel est lié récemment . La résolution de surcharge de méthode se produit au moment de l'exécution et essaie de choisir la meilleure surcharge. Donc, si vous appelez la méthode ProcessItem avec un objet de type BarItem, vous appellerez en fait la méthode non générique, car elle correspond mieux à ce type. Cependant, vous obtiendrez une erreur d'exécution lorsque vous passerez un argument du type Alpha car aucune méthode ne peut gérer cet objet (une méthode générique ayant la contrainte where T : IItem et la classe Alpha n'implémente pas cette interface). Mais c'est l'essentiel. Le compilateur ne dispose pas d'informations selon lesquelles 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 soyez aussi dynamic . Donc, si vous voulez changer l'exemple précédent en ce code:

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

alors le type de l'objet de résultat serait dynamic. En effet, le compilateur ne sait pas toujours quelle méthode sera appelée. Si vous connaissez le type de retour de l'appel de fonction, vous devez le convertir implicitement en le type requis afin que le reste du code soit typé de manière 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 lors de la deuxième itération de la boucle. Cela est dû au fait que vous avez essayé de sauvegarder la valeur de retour d’une fonction void.

128
Mariusz Pawelski

Avec C # 4.0, la réflexion n'est pas nécessaire car le DLR peut l'appeler à l'aide de types d'exécution. Puisque l’utilisation de la bibliothèque DLR est une sorte de difficulté dynamique (au lieu du compilateur C # qui génère du code pour vous), le cadre Open Source Dynamitey (.net standard 1.5) vous permet d’accéder facilement à la mise en cache à l’exécution. les mêmes appels 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}));
17
jbtule

Ajouter à réponse d'Adrian Gallero :

L'appel d'une méthode générique à partir de type info implique trois étapes.

TLDR: L’appel d’une méthode générique connue avec un objet type peut être réalisé de la manière suivante:

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

GenericMethod<object> est le nom de la méthode à appeler et tout type satisfaisant les contraintes génériques.

(Action) correspond à la signature de la méthode à appeler, c'est-à-dire (Func<string,string,int> ou Action<bool>)

L'étape 1 consiste à obtenir le MethodInfo pour la définition de la méthode générique

Méthode 1: utilisez GetMethod () ou GetMethods () avec les types appropriés ou les indicateurs de liaison.

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

Méthode 2: créez un délégué, obtenez l'objet MethodInfo, puis appelez GetGenericMethodDefinition

Depuis la classe qui contient les méthodes:

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

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

En dehors de la classe qui contient les méthodes:

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

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

En C #, le nom d’une méthode, c’est-à-dire "ToString" ou "GenericMethod", désigne en fait un groupe de méthodes pouvant contenir une ou plusieurs méthodes. Tant que vous n’indiquez pas les types de paramètres de méthode, vous ne savez pas à quelle méthode vous vous référez.

((Action)GenericMethod<object>) fait référence au délégué pour une méthode spécifique. ((Func<string, int>)GenericMethod<object>) fait référence à une surcharge différente de GenericMethod

Méthode 3: créez une expression lambda contenant une expression d'appel de méthode, obtenez l'objet MethodInfo puis GetGenericMethodDefinition

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

Cela se résume à

Créez une expression lambda où le corps est un appel à la méthode souhaitée.

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

Extraire le corps et le transtyper en MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Obtenir la définition de méthode générique à partir de la méthode

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

L'étape 2 appelle MakeGenericMethod pour créer une méthode générique avec le ou les types appropriés.

MethodInfo generic = method.MakeGenericMethod(myType);

L'étape 3 appelle la méthode avec les arguments appropriés.

generic.Invoke(this, null);
11
Grax

Personne n'a fourni la solution " classique Reflection ", voici donc un exemple de code complet:

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 classe DynamicDictionaryFactory ci-dessus a une méthode

CreateDynamicGenericInstance(Type keyType, Type valueType)

et il crée et retourne une instance IDictionary, dont les types de clés et de valeurs sont exactement les mêmes que ceux spécifiés dans l'appel keyType et valueType.

Voici un exemple complet comment appeler cette méthode pour instancier et utiliser 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");
        }
    }
}

Lorsque l'application console ci-dessus est exécutée, nous obtenons le résultat attendu correct:

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

Ceci est mes 2 cents basés sur réponse de Grax , mais avec deux paramètres requis pour une méthode générique.

Supposons que votre méthode est définie comme suit dans une classe Helpers:

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

Dans mon cas, le type U est toujours une collection observable stockant un objet de type T.

Comme mes types sont prédéfinis, je crée d’abord les objets "factices" qui représentent la collection observable (U) et l’objet qui y est stocké (T) et qui seront utilisés ci-dessous pour obtenir leur type lors de l’appel de Make.

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

Appelez ensuite GetMethod pour trouver votre fonction générique:

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

Jusqu'à présent, l'appel ci-dessus est à peu près identique à ce qui a été expliqué ci-dessus, mais avec une petite différence lorsque vous devez lui transmettre plusieurs paramètres.

Vous devez passer un tableau Type [] à la fonction MakeGenericMethod contenant les types d'objets "factices" créés ci-dessus:

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

Une fois que cela est fait, vous devez appeler la méthode Invoke comme indiqué ci-dessus.

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

Et tu as fini. Fonctionne un charme!

UPDATE:

Comme @Bevan l'a souligné, je n'ai pas besoin de créer de tableau lorsque j'appelle la fonction MakeGenericMethod car elle utilise des paramètres et je n'ai pas besoin de créer un objet pour obtenir les types, car je peux simplement les transmettre directement à cette fonction. Dans mon cas, comme j'ai les types prédéfinis dans une autre classe, j'ai simplement changé mon code en:

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 contient 2 propriétés de type Type que j'ai définies lors de l'exécution en fonction d'une valeur enum transmise au constructeur et me fourniront les types pertinents que j'utiliserai ensuite dans MakeGenericMethod.

Merci encore d'avoir souligné ce @Bevan.

2
Thierry