Question 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 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 - à l'intérieur du Example() méthode, quelle est la manière la plus concise d'invoquer GenericMethod<T>() en utilisant le Type stocké dans le myType variable?

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

844
2017-10-24 05:17


origine


Réponses:


Vous devez utiliser la réflexion pour commencer la méthode, puis "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, passez null comme le premier argument à Invoke. Cela n'a rien à voir avec les méthodes génériques - c'est juste une réflexion normale.

Comme indiqué, beaucoup de cela est plus simple à partir de C # 4 en utilisant 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 l'exemple exact dans la question.


921
2017-10-24 06:13



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

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

Il est également un peu dangereux en ce que vous perdez le contrôle de compilation pour GenericMethod. Si vous faites plus tard un refactoring et renommer GenericMethod, ce code ne remarquera pas et échouera à l'exécution. De même, s'il y a un post-traitement de l'assembly (par exemple, obscurcissement ou suppression de 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 cela n'est pas appelé des millions de fois, les frais généraux ne sont pas importants, 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 jolie, vous avez une référence de temps de compilation à GenericMethod ici, et si vous refactor, supprimer ou faire quelque chose avec GenericMethod, ce code continuera à fonctionner, ou au moins cassera 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 Activator. Je ne sais pas s'il y a un meilleur moyen.


135
2018-02-27 16:11



L'appel d'une méthode générique avec un paramètre de type connu seulement à l'exécution peut être grandement simplifié en utilisant un dynamic tapez 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 du Type classe). 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 le Activator.CreateInstance méthode.

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

En convertissant l'argument d'objet en dynamic tapez nous différé fournissant le paramètre de type jusqu'à l'exécution. Quand le Process méthode est appelée avec le dynamic argument alors le compilateur ne se soucie pas du type de cet argument. Le compilateur génère du code qui, lors de l'exécution, vérifie les types réels d'arguments passés (en utilisant la réflexion) et choisit la meilleure méthode à appeler. Ici, il n'y a qu'une seule méthode générique, donc elle est invoquée avec un paramètre de type correct.

Dans cet exemple, la sortie est la même que si vous écriviez:

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. Vous ne devriez pas non plus vous soucier des performances de l'appel de cette fonction plusieurs fois. Le prochain appel avec des arguments du même type devrait être plus rapide grâce au mise en cache mécanisme dans DLR. Bien sûr, vous pouvez écrire du code qui invoque les délégués, mais en utilisant le dynamic tapez 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 auxiliaire 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é du type

Quel est vraiment génial d'utiliser dynamic objet en remplacement de l'utilisation de l'API de réflexion est que vous perdez seulement la vérification de temps de compilation de ce type particulier que vous ne connaissez pas avant 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, changez leur type ou renommez le nom de la méthode, vous obtiendrez une erreur de compilation. Cela n'arrivera pas si vous fournissez le nom de la méthode sous forme de chaîne Type.GetMethod et arguments comme le 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 lors de l'exécution. Il montre également comment le DLR essaie 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 plaçant l'argument à la dynamic type. 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 le dynamic argument à une méthode alors cet appel est récemment lié. 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 invoquez le ProcessItem méthode avec un objet de BarItem tapez alors vous appellerez réellement la méthode non générique, parce que c'est une meilleure correspondance pour ce type. Cependant, vous obtiendrez une erreur d'exécution lorsque vous passez un argument de la Alpha type car il n'existe aucune méthode capable de gérer cet objet (une méthode générique a la contrainte where T : IItem et Alpha classe n'implémente pas cette interface). Mais c'est tout le problème. Le compilateur n'a pas d'informations que cet appel est valide. En tant que programmeur, vous le savez, et vous devez vous assurer que ce code fonctionne 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 aussi. Donc, si vous changez l'exemple précédent pour ce code:

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

alors le type de l'objet résultat serait dynamic. C'est parce que 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 devriez convertir implicitement le type requis pour que le reste du code soit typé statiquement:

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 de résultat dans l'exemple précédent, vous obtiendrez une erreur d'exécution dans l'itération de la deuxième boucle. C'est parce que vous avez essayé d'enregistrer la valeur de retour d'une fonction vide.


105
2018-03-16 19:21



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. Depuis l'utilisation de la bibliothèque DLR est une sorte de douleur dynamiquement (au lieu du code générant le compilateur C # pour vous), le framework open source Dynamitey (.net standard 1.5) vous donne un accès facile à l'exécution en cache aux 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}));

11
2017-07-05 13:40



Ajouter à La réponse d'Adrian Gallero:

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

TLDR: appeler une méthode générique connue avec un objet type peut être accompli par:

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

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

(Action) correspond à la signature de la méthode à appeler i.e. (Func<string,string,int> ou Action<bool>)

L'étape 1 obtient le MethodInfo pour la définition de 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é, récupérez l'objet MethodInfo, puis appelez GetGenericMethodDefinition

De l'intérieur de la classe qui contient les méthodes:

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

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

De l'extérieur 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" fait en réalité référence à un groupe de méthodes pouvant contenir une ou plusieurs méthodes. Jusqu'à ce que vous fournissiez les types de paramètres de la méthode, on ne sait pas méthode dont vous parlez.

((Action)GenericMethod<object>) se réfère 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éer une expression lambda contenant une expression d'appel de méthode, obtenir l'objet MethodInfo, puis GetGenericMethodDefinition

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

Cela se décompose en

Créer une expression lambda où le corps est un appel à la méthode désirée.

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

Extraire le corps et convertir 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 (s) type (s) approprié (s).

MethodInfo generic = method.MakeGenericMethod(myType);

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

generic.Invoke(this, null);

5
2018-01-09 22:20



Personne n'a fourni le "Réflexion classique"solution, 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;

        }
    }
}

Ce qui précède DynamicDictionaryFactory classe a une méthode

CreateDynamicGenericInstance(Type keyType, Type valueType)

et il crée et renvoie une instance IDictionary, dont les types de clés et de valeurs sont exactement 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 de console ci-dessus est exécutée, nous obtenons le résultat correct et attendu:

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

2
2017-08-24 01:24



C'est mon 2 cents basé sur La 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 stocké dans celle-ci (T) et qui seront utilisés ci-dessous pour obtenir leur type lors de l'appel du Make

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

Puis appelez le 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 passer plusieurs paramètres.

Vous devez passer un tableau Type [] à la fonction MakeGenericMethod qui contient les types d'objets "fictifs" qui ont été créés ci-dessus:

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

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

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

Et tu as fini. Fonctionne un charme!

METTRE À JOUR:

Comme @Bevan l'a souligné, je n'ai pas besoin de créer un tableau quand j'appelle la fonction MakeGenericMethod comme il le fait dans params et je n'ai pas besoin de créer un objet pour obtenir les types car je peux simplement passer les types directement à cette fonction. Dans mon cas, comme j'ai les types prédéfinis dans une autre classe, j'ai simplement changé mon code pour:

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éfini à l'exécution en fonction d'une valeur enum passée au constructeur et qui me fournira les types pertinents que j'utiliserai ensuite dans MakeGenericMethod.

Merci encore de mettre en évidence ce @Bevan.


0
2017-10-22 23:28