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>()
{
//...
}
}
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.
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.
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) + ">");
}
}
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.
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.
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}));
Ajouter à réponse d'Adrian Gallero :
L'appel d'une méthode générique à partir de type info implique trois étapes.
((Action)GenericMethod<object>)
.Method
.GetGenericMethodDefinition()
.MakeGenericMethod(typeof(string))
.Invoke(this, null);
où 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>
)
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
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
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();
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
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
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.