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 dynamic
tipo 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 Type
classe). 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 typeof
operador).
Ao converter o argumento do objeto para dynamic
digitar, adiamos o fornecimento do parâmetro type até o tempo de execução. Quando o Process
método é chamado com o dynamic
argumento, 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 dynamic
tipo, 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 dynamic
objeto 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.GetMethod
e 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 dynamic
tipo 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 dynamic
argumento 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 ProcessItem
método com um objeto do BarItem
tipo, 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 Alpha
tipo porque não há um método que possa manipular esse objeto (um método genérico possui a restrição where T : IItem
e a Alpha
classe 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.