Como uso a reflexão para chamar um método genérico?

1071

Qual é a melhor maneira de chamar um método genérico quando o parâmetro type não é conhecido no tempo de compilação, mas é obtido dinamicamente no tempo de execução?

Considere o seguinte código de exemplo - dentro do Example()método, qual é a maneira mais concisa de chamar GenericMethod<T>()usando o Typearmazenado na myTypevariável?

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>()
    {
        //...
    }
}
Bevan
fonte
7
Tentei a solução de Jon e não consegui fazê-la funcionar até tornar público o método genérico em minha classe. Eu sei que outro Jon respondeu dizendo que você precisa especificar os bindingflags, mas isso não ajudou.
naskew
12
Você também precisa BindingFlags.Instance, não apenas BindingFlags.NonPublic, de obter o método privado / interno.
Lars Kemmann
2
Versão moderna desta pergunta: stackoverflow.com/q/2433436/103167
Ben Voigt
@ Peter Mortensen - eu usei espaços antes do '?' separar as partes em inglês das partes em inglês (C #); IMHO remover o espaço faz com que pareça com o? faz parte do código. Se não havia nenhum código, eu certamente concordo com removendo os espaços, mas neste caso ...
Bevan

Respostas:

1139

Você precisa usar a reflexão para iniciar o método e, em seguida, "construí-lo" fornecendo argumentos de tipo com MakeGenericMethod :

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

Para um método estático, passe nullcomo o primeiro argumento para Invoke. Isso não tem nada a ver com métodos genéricos - é apenas uma reflexão normal.

Como observado, muito disso é mais simples a partir do C # 4 dynamic- se é possível usar a inferência de tipo, é claro. Isso não ajuda nos casos em que a inferência de tipo não está disponível, como o exemplo exato da pergunta.

Jon Skeet
fonte
92
+1; observe que GetMethod()considera apenas os métodos de instância pública por padrão; portanto, você pode precisar BindingFlags.Statice / ou BindingFlags.NonPublic.
20
A combinação correta de sinalizadores é BindingFlags.NonPublic | BindingFlags.Instance(e opcionalmente BindingFlags.Static).
Lars Kemmann 15/02
4
Uma pergunta que se destaca como duvidosa se pergunta como fazer isso com métodos estáticos - e tecnicamente a pergunta aqui. O primeiro parâmetro de generic.Invoke () deve ser nulo ao chamar métodos estáticos. O primeiro parâmetro é necessário apenas ao chamar métodos de instância.
Chris Moschini 22/03
2
@ ChrisMoschini: Adicionado isso à resposta.
precisa
2
@ gzou: eu adicionei algo à resposta - mas observe que, ao chamar os métodos genéricos na pergunta , dynamicnão ajuda porque a inferência de tipo não está disponível. (Não há argumentos que o compilador pode usar para determinar o tipo de argumento.)
Jon Skeet
170

Apenas uma adição à resposta original. Enquanto isso vai funcionar:

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

Também é um pouco perigoso, pois você perde a verificação em tempo de compilação GenericMethod. Se você posteriormente refatorar e renomear GenericMethod, esse código não notará e falhará no tempo de execução. Além disso, se houver algum pós-processamento do assembly (por exemplo, ofuscar ou remover métodos / classes não utilizados), esse código também poderá ser quebrado.

Portanto, se você conhece o método ao qual está vinculando no momento da compilação, e isso não é chamado milhões de vezes, então a sobrecarga não importa, eu alteraria esse código para:

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

Embora não seja muito bonito, você tem uma referência em tempo de compilação GenericMethodaqui e, se você refatorar, excluir ou fazer qualquer coisa com GenericMethod, esse código continuará funcionando ou, pelo menos, será interrompido no tempo de compilação (se você remover por exemplo GenericMethod).

Outra maneira de fazer o mesmo seria criar uma nova classe de wrapper e criá-la Activator. Não sei se existe uma maneira melhor.

Adrian Gallero
fonte
5
Nos casos em que a reflexão é usada para chamar um método, é comum que o próprio nome do método seja descoberto por outro método. Saber o nome do método com antecedência não é comum.
Bevan
13
Bem, concordo com usos comuns de reflexão. Mas a pergunta original era como chamar "GenericMethod <myType> ()" Se essa sintaxe fosse permitida, não precisaríamos de GetMethod (). Mas para a pergunta "como eu escrevo" GenericMethod <myType> "? Acho que a resposta deve incluir uma maneira de evitar a perda do link em tempo de compilação com o GenericMethod. Agora, se essa pergunta é comum ou não, não sei, mas Eu sei que eu tinha esse problema exato ontem, e é por isso que desembarcou nesta questão.
Adrian Gallero
20
Você poderia fazer em GenMethod.Method.GetGenericMethodDefinition()vez de this.GetType().GetMethod(GenMethod.Method.Name). É um pouco mais limpo e provavelmente mais seguro.
Daniel Cassidy
O que significa "myType" na sua amostra?
Desenvolvedor
37
Agora você pode usarnameof(GenericMethod)
dmigo 17/03/16
140

Chamar um método genérico com um parâmetro de tipo conhecido apenas em tempo de execução pode ser bastante simplificado usando um dynamictipo em vez da API de reflexão.

Para usar essa técnica, o tipo deve ser conhecido a partir do objeto real (não apenas uma instância da Typeclasse). Caso contrário, você precisará criar um objeto desse tipo ou usar a solução API de reflexão padrão . Você pode criar um objeto usando o método Activator.CreateInstance .

Se você deseja chamar um método genérico, que no uso "normal" teria seu tipo inferido, basta converter o objeto de tipo desconhecido para dynamic. Aqui está um exemplo:

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

E aqui está a saída deste programa:

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é um método de instância genérico que grava o tipo real do argumento passado (usando o GetType()método) e o tipo do parâmetro genérico (usando o typeofoperador).

Ao converter o argumento do objeto para dynamicdigitar, adiamos o fornecimento do parâmetro type até o tempo de execução. Quando o Processmétodo é chamado com o dynamicargumento, o compilador não se importa com o tipo desse argumento. O compilador gera código que, em tempo de execução, verifica os tipos reais de argumentos passados ​​(usando reflexão) e escolhe o melhor método para chamar. Aqui existe apenas esse método genérico, portanto, ele é chamado com um parâmetro de tipo apropriado.

Neste exemplo, a saída é a mesma que se você tivesse escrito:

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

A versão com um tipo dinâmico é definitivamente mais curta e fácil de escrever. Você também não deve se preocupar com o desempenho de chamar essa função várias vezes. A próxima chamada com argumentos do mesmo tipo deve ser mais rápida, graças ao mecanismo de cache no DLR. Obviamente, você pode escrever um código que armazene em cache os delegados invocados, mas, usando o dynamictipo, você obtém esse comportamento gratuitamente.

Se o método genérico que você deseja chamar não tiver um argumento de um tipo parametrizado (portanto, seu parâmetro de tipo não pode ser inferido), você poderá agrupar a invocação do método genérico em um método auxiliar, como no exemplo a seguir:

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

Maior segurança do tipo

O que é realmente bom em usar o dynamicobjeto como um substituto para o uso da API de reflexão é que você só perde a verificação do tempo de compilação desse tipo específico que você não conhece até o tempo de execução. Outros argumentos e o nome do método são analisados ​​estaticamente pelo compilador, como de costume. Se você remover ou adicionar mais argumentos, alterar seus tipos ou renomear o nome do método, você receberá um erro em tempo de compilação. Isso não acontecerá se você fornecer o nome do método como uma string Type.GetMethode argumentos conforme a matriz de objetos MethodInfo.Invoke.

Abaixo está um exemplo simples que ilustra como alguns erros podem ser detectados em tempo de compilação (código comentado) e outros em tempo de execução. Também mostra como o DLR tenta resolver qual método chamar.

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

Aqui, novamente, executamos algum método lançando o argumento para o dynamictipo Somente a verificação do tipo do primeiro argumento é adiada para o tempo de execução. Você receberá um erro do compilador se o nome do método que você está chamando não existir ou se outros argumentos forem inválidos (número errado de argumentos ou tipos errados).

Quando você passa o dynamicargumento para um método, essa chamada é vinculada ultimamente . A resolução de sobrecarga do método ocorre no tempo de execução e tenta escolher a melhor sobrecarga. Portanto, se você chamar o ProcessItemmétodo com um objeto do BarItemtipo, na verdade, chamará o método não genérico, porque é uma correspondência melhor para esse tipo. No entanto, você receberá um erro de tempo de execução ao passar um argumento do Alphatipo porque não há um método que possa manipular esse objeto (um método genérico possui a restrição where T : IIteme a Alphaclasse não implementa essa interface). Mas esse é o ponto. O compilador não possui informações de que esta chamada é válida. Você, como programador, sabe disso e deve garantir que esse código seja executado sem erros.

Gotcha do tipo de retorno

Quando você está chamando um método não nulo com um parâmetro do tipo dinâmico, seu tipo de retorno provavelmente também serádynamic . Portanto, se você alterar o exemplo anterior para este código:

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

então o tipo do objeto de resultado seria dynamic. Isso ocorre porque o compilador nem sempre sabe qual método será chamado. Se você conhece o tipo de retorno da chamada de função, deve convertê- lo implicitamente no tipo necessário, para que o restante do código seja digitado estaticamente:

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

Você receberá um erro de tempo de execução se o tipo não corresponder.

Na verdade, se você tentar obter o valor do resultado no exemplo anterior, receberá um erro de tempo de execução na segunda iteração do loop. Isso ocorre porque você tentou salvar o valor de retorno de uma função nula.

Mariusz Pawelski
fonte
Mariusz, confuso com "No entanto, você receberá um erro de tempo de execução ao passar o argumento do tipo Alpha, porque não há um método que possa manipular esse objeto." Se eu chamar var a = new Alpha () ProcessItem (a, "test" + i , i) Por que o método ProcessItem genérico não trataria isso efetivamente, produzindo "Item de Processo Geral"?
precisa
@AlexEdelstein Eu editei minha resposta para esclarecer um pouco. É porque o ProcessItemmétodo genérico possui restrição genérica e aceita apenas objetos que implementam a IIteminterface. Quando você ligará ProcessItem(new Aplha(), "test" , 1);ou ProcessItem((object)(new Aplha()), "test" , 1);obterá um erro do compilador, mas ao transmitir para dynamicvocê adiar essa verificação para o tempo de execução.
Mariusz Pawelski 23/03
Ótima resposta e explicação, funciona perfeitamente para mim. Muito melhor que a resposta aceita, mais curta para escrever, mais eficiente e mais segura.
ygoe 28/08/2015
17

Com o C # 4.0, a reflexão não é necessária, como o DLR pode chamá-lo usando tipos de tempo de execução. Como o uso da biblioteca DLR é meio problemático dinamicamente (em vez do compilador C # que gera código para você), a estrutura de código aberto Dynamitey (.net padrão 1.5) oferece fácil acesso em tempo de execução em cache às mesmas chamadas que o compilador geraria para voce.

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


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

Acrescentando à resposta de Adrian Gallero :

Chamar um método genérico a partir de informações do tipo envolve três etapas.

TLDR: A chamada de um método genérico conhecido com um objeto de tipo pode ser realizada por:

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

Onde GenericMethod<object> é o nome do método a ser chamado e qualquer tipo que atenda às restrições genéricas.

(Ação) corresponde à assinatura do método a ser chamado, ou seja, ( Func<string,string,int>ou Action<bool>)

Etapa 1 é obter o MethodInfo para a definição genérica do método

Método 1: Use GetMethod () ou GetMethods () com tipos apropriados ou sinalizadores de ligação.

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

Método 2: Crie um delegado, obtenha o objeto MethodInfo e chame GetGenericMethodDefinition

De dentro da classe que contém os métodos:

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

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

De fora da classe que contém os métodos:

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

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

Em C #, o nome de um método, ou seja, "ToString" ou "GenericMethod", na verdade, refere-se a um grupo de métodos que podem conter um ou mais métodos. Até você fornecer os tipos dos parâmetros do método, não se sabe a qual método você está se referindo.

((Action)GenericMethod<object>) refere-se ao delegado para um método específico. ((Func<string, int>)GenericMethod<object>) refere-se a uma sobrecarga diferente de GenericMethod

Método 3: Crie uma expressão lambda contendo uma expressão de chamada de método, obtenha o objeto MethodInfo e, em seguida, GetGenericMethodDefinition

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

Isso se divide em

Crie uma expressão lambda em que o corpo é uma chamada para o método desejado.

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

Extraia o corpo e faça a conversão para MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Obtenha a definição genérica do método no método

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

A etapa 2 está chamando MakeGenericMethod para criar um método genérico com o (s) tipo (s) apropriado (s).

MethodInfo generic = method.MakeGenericMethod(myType);

A etapa 3 é invocar o método com os argumentos apropriados.

generic.Invoke(this, null);
Grax32
fonte
8

Ninguém forneceu a solução " Reflexão clássica ", então aqui está um exemplo de código 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;

        }
    }
}

A DynamicDictionaryFactoryclasse acima tem um método

CreateDynamicGenericInstance(Type keyType, Type valueType)

e cria e retorna uma instância do IDictionary, cujos tipos de chaves e valores são exatamente os especificados na chamada keyTypeevalueType .

Aqui está um exemplo completo de como chamar esse método para instanciar e usar um 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 o aplicativo de console acima é executado, obtemos o resultado correto e esperado:

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

Este é o meu 2 centavos com base na resposta de Grax , mas com dois parâmetros necessários para um método genérico.

Suponha que seu método seja definido da seguinte maneira em uma classe Helpers:

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

No meu caso, o tipo U é sempre uma coleção observável que armazena objetos do tipo T.

Como tenho meus tipos predefinidos, primeiro crio os objetos "fictícios" que representam a coleção observável (U) e o objeto armazenado nela (T) e que serão usados ​​abaixo para obter seu tipo ao chamar o Make

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

Em seguida, chame o GetMethod para encontrar sua função genérica:

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

Até agora, a chamada acima é praticamente idêntica ao que foi explicado acima, mas com uma pequena diferença quando você precisa passar vários parâmetros para ela.

Você precisa passar uma matriz Type [] para a função MakeGenericMethod que contém os tipos de objetos "fictícios" criados acima:

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

Feito isso, você precisa chamar o método Invoke, conforme mencionado acima.

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

E você terminou. Funciona um encanto!

ATUALIZAR:

Como o @Bevan destacou, não preciso criar uma matriz ao chamar a função MakeGenericMethod, pois utiliza params e não preciso criar um objeto para obter os tipos, pois posso passar os tipos diretamente para esta função. No meu caso, como tenho os tipos predefinidos em outra classe, simplesmente mudei meu código para:

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 contém 2 propriedades do tipo Type que eu defino em tempo de execução com base em um valor de enum passado para o construtor e fornecerá os tipos relevantes que eu uso no MakeGenericMethod.

Mais uma vez obrigado por destacar este @Bevan.

Thierry
fonte
Os argumentos para MakeGenericMethod()ter a palavra-chave params para que você não precise criar uma matriz; nem você precisa criar instâncias para obter os tipos - methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject))seria suficiente.
Bevan
0

Inspirado pela resposta da Enigmativity - vamos supor que você tenha duas (ou mais) classes, como

public class Bar { }
public class Square { }

e você deseja chamar o método Foo<T>com Bare Square, que é declarado como

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

Então você pode implementar um método de extensão como:

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

Com isso, você pode simplesmente invocar Foocomo:

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

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

que funciona para todas as classes. Nesse caso, ele produzirá:

Square
Bar

Matt
fonte