Come posso usare la riflessione per chiamare un metodo generico?


1071

Qual è il modo migliore per chiamare un metodo generico quando il parametro type non è noto al momento della compilazione, ma viene invece ottenuto dinamicamente in fase di esecuzione?

Considera il seguente codice di esempio: all'interno del Example()metodo, qual è il modo più conciso per invocare GenericMethod<T>()usando l' Typearchiviato nella myTypevariabile?

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

7
Ho provato la soluzione di Jon e non sono riuscito a farlo funzionare fino a quando non ho reso pubblico il metodo generico nella mia classe. So che un altro Jon ha risposto dicendo che è necessario specificare le bandiere vincolanti ma questo non ha aiutato.
naskew

12
È inoltre necessario BindingFlags.Instance, non solo BindingFlags.NonPublic, ottenere il metodo privato / interno.
Lars Kemmann,

2
Versione moderna di questa domanda: stackoverflow.com/q/2433436/103167
Ben Voigt,

@Peter Mortensen - A proposito, ho usato gli spazi prima del '?' separare le parti inglesi dalle parti non inglesi (C #); IMHO rimuovendo lo spazio lo fa sembrare il? fa parte del codice. Se non ci fosse codice, sarei sicuramente d'accordo con la rimozione degli spazi, ma in questo caso ...
Bevan,

Risposte:


1139

È necessario utilizzare reflection per ottenere il metodo con cui iniziare, quindi "costruirlo" fornendo argomenti di tipo con MakeGenericMethod :

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

Per un metodo statico, passare nullcome primo argomento a Invoke. Non ha nulla a che fare con i metodi generici: è solo una normale riflessione.

Come notato, molto di questo è più semplice a partire dall'uso di C # 4 dynamic- se puoi usare l'inferenza di tipo, ovviamente. Non aiuta nei casi in cui l'inferenza del tipo non è disponibile, come nell'esempio esatto nella domanda.


92
+1; si noti che GetMethod()considera solo i metodi di istanza pubblica per impostazione predefinita, quindi potrebbe essere necessario BindingFlags.Statice / o BindingFlags.NonPublic.

20
La combinazione corretta di flag è BindingFlags.NonPublic | BindingFlags.Instance(e facoltativamente BindingFlags.Static).
Lars Kemmann,

4
Una domanda che si contraddistingue per questo si chiede come farlo con metodi statici - e tecnicamente anche la domanda qui. Il primo parametro di generic.Invoke () dovrebbe essere nullo quando si chiamano metodi statici. Il primo parametro è necessario solo quando si chiamano i metodi di istanza.
Chris Moschini,

2
@ChrisMoschini: aggiunto alla risposta.
Jon Skeet,

2
@gzou: ho aggiunto qualcosa alla risposta, ma nota che per chiamare i metodi generici nella domanda , dynamicnon aiuta perché l'inferenza del tipo non è disponibile. (Non ci sono argomenti che il compilatore può usare per determinare l'argomento type.)
Jon Skeet

170

Solo un'aggiunta alla risposta originale. Mentre questo funzionerà:

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

È anche un po 'pericoloso in quanto si perde il controllo del tempo di compilazione per GenericMethod. Se successivamente esegui un refactoring e rinominaGenericMethod , questo codice non noterà e fallirà in fase di esecuzione. Inoltre, in caso di post-elaborazione dell'assembly (ad esempio offuscamento o rimozione di metodi / classi non utilizzati) anche questo codice potrebbe non funzionare.

Quindi, se conosci il metodo che stai collegando al momento della compilazione, e questo non viene chiamato milioni di volte, quindi l'overhead non ha importanza, cambierei questo codice in:

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

Anche se non molto carino, hai un riferimento al tempo di compilazione GenericMethodqui, e se fai refactoring, elimini o fai qualcosa con GenericMethod, questo codice continuerà a funzionare, o almeno si interromperà al momento della compilazione (se ad esempio rimuovi GenericMethod).

Un altro modo di fare lo stesso sarebbe creare una nuova classe wrapper e crearla Activator. Non so se esiste un modo migliore.


5
Nei casi in cui la riflessione viene utilizzata per chiamare un metodo, è normale che il nome del metodo venga scoperto da un altro metodo. Conoscere il nome del metodo in anticipo non è comune.
Bevan,

13
Bene, sono d'accordo per gli usi comuni della riflessione. Ma la domanda originale era come chiamare "GenericMethod <myType> ()" Se quella sintassi fosse consentita, non avremmo affatto bisogno di GetMethod (). Ma per la domanda "come faccio a scrivere" GenericMethod <myType> "? Penso che la risposta dovrebbe includere un modo per evitare di perdere il collegamento in fase di compilazione con GenericMethod. Ora se questa domanda è comune o no non lo so, ma So di aver avuto esattamente questo problema ieri, ed è per questo che sono arrivato a questa domanda.
Adrian Gallero,

20
Potresti fare GenMethod.Method.GetGenericMethodDefinition()invece di this.GetType().GetMethod(GenMethod.Method.Name). È leggermente più pulito e probabilmente più sicuro.
Daniel Cassidy,

Cosa significa "myType" nel tuo campione?
Sviluppatore

37
Ora puoi usarenameof(GenericMethod)
dmigo

140

La chiamata di un metodo generico con un parametro di tipo noto solo in fase di esecuzione può essere notevolmente semplificata utilizzando a dynamic tipo anziché l'API di reflection.

Per utilizzare questa tecnica è necessario conoscere il tipo dall'oggetto reale (non solo un'istanza della Typeclasse). Altrimenti, è necessario creare un oggetto di quel tipo o utilizzare la soluzione API di riflessione standard . È possibile creare un oggetto utilizzando Activator.CreateInstance metodo .

Se si desidera chiamare un metodo generico, che nell'uso "normale" si sarebbe dedotto il suo tipo, allora si tratta semplicemente di lanciare l'oggetto di tipo sconosciuto dynamic. Ecco un esempio:

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

Ed ecco l'output di questo programma:

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è un metodo di istanza generico che scrive il tipo reale dell'argomento passato (usando il GetType()metodo) e il tipo del parametro generico (usando l' typeofoperatore).

Trasmettendo l'argomento object da dynamicdigitare abbiamo rinviato fornendo il parametro type fino al runtime. Quando il Processmetodo viene chiamato con ildynamic argomento, al compilatore non interessa il tipo di argomento. Il compilatore genera codice che in fase di runtime verifica i tipi reali di argomenti passati (usando la riflessione) e sceglie il metodo migliore da chiamare. Qui esiste solo questo metodo generico, quindi viene invocato con un parametro di tipo appropriato.

In questo esempio, l'output è lo stesso di se si scrivesse:

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

La versione con un tipo dinamico è decisamente più breve e più facile da scrivere. Inoltre, non dovresti preoccuparti delle prestazioni di chiamata a questa funzione più volte. La chiamata successiva con argomenti dello stesso tipo dovrebbe essere più veloce grazie al meccanismo di memorizzazione nella cache in DLR. Ovviamente, puoi scrivere codice che memorizza nella cache i delegati invocati, ma utilizzando il dynamictipo ottieni questo comportamento gratuitamente.

Se il metodo generico che vuoi chiamare non ha un argomento di tipo parametrizzato (quindi il suo parametro di tipo non può essere dedotto), puoi avvolgere l'invocazione del metodo generico in un metodo di supporto come nell'esempio seguente:

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

Maggiore sicurezza del tipo

La cosa fantastica dell'utilizzo dynamicdell'oggetto come sostituto dell'utilizzo dell'API reflection è che si perde solo il controllo della compilazione di questo particolare tipo che non si conosce fino al runtime. Altri argomenti e il nome del metodo vengono analizzati staticamente dal compilatore come al solito. Se rimuovi o aggiungi altri argomenti, ne cambi i tipi o rinomini il nome del metodo, otterrai un errore di compilazione. Ciò non accadrà se si fornisce il nome del metodo come stringa Type.GetMethode argomenti come l'array di oggetti MethodInfo.Invoke.

Di seguito è riportato un semplice esempio che illustra come è possibile rilevare alcuni errori in fase di compilazione (codice commentato) e altri in fase di esecuzione. Mostra anche come il DLR tenta di risolvere quale metodo chiamare.

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

Qui eseguiamo nuovamente un metodo lanciando l'argomento sul dynamictipo. Solo la verifica del tipo del primo argomento è posticipata al runtime. Riceverai un errore del compilatore se il nome del metodo che stai chiamando non esiste o se altri argomenti non sono validi (numero errato di argomenti o tipi errati).

Quando si passa l' dynamicargomento a un metodo, questa chiamata viene recentemente associata . La risoluzione del sovraccarico del metodo si verifica in fase di esecuzione e cerca di scegliere il sovraccarico migliore. Quindi, se invochi il ProcessItemmetodo con un oggetto di BarItemtipo, in realtà chiamerai il metodo non generico, perché è una corrispondenza migliore per questo tipo. Tuttavia, otterrai un errore di runtime quando passi un argomento del Alphatipo perché non esiste un metodo in grado di gestire questo oggetto (un metodo generico ha il vincolo where T : IIteme la Alphaclasse non implementa questa interfaccia). Ma questo è il punto. Il compilatore non ha informazioni sulla validità di questa chiamata. Come programmatore lo sai e dovresti assicurarti che questo codice venga eseguito senza errori.

Tipo di ritorno gotcha

Quando chiami un metodo non vuoto con un parametro di tipo dinamico, probabilmente lo sarà dynamicanche il suo tipo restituito . Quindi, se cambiassi l'esempio precedente in questo codice:

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

quindi il tipo dell'oggetto risultato sarebbe dynamic. Questo perché il compilatore non sempre sa quale metodo verrà chiamato. Se si conosce il tipo restituito della chiamata di funzione, è necessario convertirlo in modo implicito nel tipo richiesto in modo che il resto del codice venga digitato staticamente:

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

Riceverai un errore di runtime se il tipo non corrisponde.

In realtà, se si tenta di ottenere il valore del risultato nell'esempio precedente, si otterrà un errore di runtime nella seconda iterazione del ciclo. Questo perché si è tentato di salvare il valore restituito di una funzione void.


Mariusz, confuso da "Tuttavia, riceverai un errore di runtime quando passi argomenti di tipo Alpha perché non esiste un metodo in grado di gestire questo oggetto." Se chiamo var a = new Alpha () ProcessItem (a, "test" + i , i) Perché il metodo ProcessItem generico non dovrebbe gestirlo in modo efficace, producendo "Elemento di processo generale"?
Alex Edelstein,

@AlexEdelstein Ho modificato la mia risposta per chiarire un po '. È perché il ProcessItemmetodo generico ha un vincolo generico e accetta solo oggetti che implementano l' IIteminterfaccia. Quando chiamerai ProcessItem(new Aplha(), "test" , 1);o ProcessItem((object)(new Aplha()), "test" , 1);otterrai un errore del compilatore ma quando esegui il casting dynamicrimandare il controllo al runtime.
Mariusz Pawelski,

Ottima risposta e spiegazione, funziona perfettamente per me. Molto meglio della risposta accettata, più breve da scrivere, più performante e più sicura.
giovedì

17

Con C # 4.0, la riflessione non è necessaria in quanto il DLR può chiamarlo utilizzando i tipi di runtime. Poiché l'utilizzo della libreria DLR è una specie di problema in modo dinamico (invece del compilatore C # che genera codice per te), il framework open source Dynamitey (.net standard 1.5) ti dà un facile accesso in fase di runtime alle stesse chiamate che il compilatore genererebbe per te.

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


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

13

Aggiungendo alla risposta di Adrian Gallero :

La chiamata di un metodo generico dalle informazioni sul tipo comporta tre passaggi.

TLDR: la chiamata di un metodo generico noto con un oggetto type può essere eseguita da:

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

dove GenericMethod<object>è il nome del metodo da chiamare e qualsiasi tipo che soddisfi i vincoli generici.

(Azione) corrisponde alla firma del metodo da chiamare ovvero ( Func<string,string,int>o Action<bool>)

Il passaggio 1 sta ottenendo il MethodInfo per la definizione del metodo generico

Metodo 1: Utilizzare GetMethod () o GetMethods () con tipi appropriati o flag di associazione.

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

Metodo 2: creare un delegato, ottenere l'oggetto MethodInfo e quindi chiamare GetGenericMethodDefinition

Dall'interno della classe che contiene i metodi:

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

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

Dall'esterno della classe che contiene i metodi:

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

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

In C #, il nome di un metodo, ad esempio "ToString" o "GenericMethod", in realtà si riferisce a un gruppo di metodi che possono contenere uno o più metodi. Fino a quando non fornisci i tipi dei parametri del metodo, non è noto a quale metodo ti riferisci.

((Action)GenericMethod<object>)si riferisce al delegato per un metodo specifico. ((Func<string, int>)GenericMethod<object>) si riferisce a un sovraccarico diverso di GenericMethod

Metodo 3: creare un'espressione lambda contenente un'espressione di chiamata del metodo, ottenere l'oggetto MethodInfo e quindi GetGenericMethodDefinition

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

Questo si rompe a

Crea un'espressione lambda in cui il corpo è una chiamata al metodo desiderato.

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

Estrarre il corpo e trasmettere a MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Ottieni la definizione del metodo generico dal metodo

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

Il passaggio 2 chiama MakeGenericMethod per creare un metodo generico con i tipi appropriati.

MethodInfo generic = method.MakeGenericMethod(myType);

Il passaggio 3 sta invocando il metodo con gli argomenti appropriati.

generic.Invoke(this, null);

8

Nessuno ha fornito la soluzione " riflessione classica ", quindi ecco un esempio di codice completo:

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 DynamicDictionaryFactoryclasse sopra ha un metodo

CreateDynamicGenericInstance(Type keyType, Type valueType)

e crea e restituisce un'istanza IDictionary, i tipi di cui chiavi e valori sono esattamente quelli specificati nella chiamata keyTypee valueType.

Ecco un esempio completo di come chiamare questo metodo per creare un'istanza e utilizzare 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");
        }
    }
}

Quando viene eseguita l'applicazione console sopra, otteniamo il risultato corretto e previsto:

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

2

Questi sono i miei 2 centesimi basati sulla risposta di Grax , ma con due parametri richiesti per un metodo generico.

Supponi che il tuo metodo sia definito come segue in una classe Helpers:

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

Nel mio caso, il tipo U è sempre una raccolta osservabile che memorizza un oggetto di tipo T.

Dato che ho i miei tipi predefiniti, per prima cosa creo gli oggetti "fittizi" che rappresentano la collezione osservabile (U) e l'oggetto in esso memorizzato (T) e che verranno utilizzati di seguito per ottenere il loro tipo quando si chiama il Make

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

Quindi chiama GetMethod per trovare la tua funzione generica:

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

Finora, la chiamata di cui sopra è praticamente identica a ciò che è stato spiegato sopra, ma con una piccola differenza quando è necessario passare più parametri ad esso.

È necessario passare un array Type [] alla funzione MakeGenericMethod che contiene i tipi di oggetti "fittizi" creati in precedenza:

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

Una volta fatto, è necessario chiamare il metodo Invoke come menzionato sopra.

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

E hai finito. Funziona un incanto!

AGGIORNARE:

Come evidenziato da @Bevan, non è necessario creare un array quando si chiama la funzione MakeGenericMethod in quanto richiede parametri e non è necessario creare un oggetto per ottenere i tipi in quanto posso semplicemente passare i tipi direttamente a questa funzione. Nel mio caso, poiché ho i tipi predefiniti in un'altra classe, ho semplicemente cambiato il mio codice in:

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 contiene 2 proprietà di tipo Typeche ho impostato in fase di esecuzione in base a un valore enum passato al costruttore e mi fornirà i tipi pertinenti che utilizzo quindi in MakeGenericMethod.

Grazie ancora per aver messo in evidenza questo @Bevan.


Gli argomenti per MakeGenericMethod()avere la parola chiave params quindi non è necessario creare un array; né è necessario creare istanze per ottenere i tipi - methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject))sarebbe sufficiente.
Bevan,

0

Ispirato dalla risposta di Enigmativity - supponiamo che tu abbia due (o più) classi, come

public class Bar { }
public class Square { }

e si desidera chiamare il metodo Foo<T>con Bare Square, che è dichiarato come

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

Quindi è possibile implementare un metodo di estensione come:

public static class Extension
{
    public static void InvokeFoo<T>(this T t)
    {
        var fooMethod = typeof(myClass).GetMethod("Foo");
        var tType = typeof(T);
        var fooTMethod = fooMethod.MakeGenericMethod(new[] { tType });
        fooTMethod.Invoke(new myClass(), new object[] { t });
    }
}

Con questo, puoi semplicemente invocare Foocome:

var objSquare = new Square();
objSquare.InvokeFoo();

var objBar = new Bar();
objBar.InvokeFoo();

che funziona per ogni classe. In questo caso, genererà:

Square
Bar

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.