Llamar a un método genérico con un parámetro de tipo conocido solo en tiempo de ejecución puede simplificarse enormemente usando un dynamic
tipo en lugar de la API de reflexión.
Para usar esta técnica, el tipo debe conocerse desde el objeto real (no solo una instancia de la Type
clase). De lo contrario, debe crear un objeto de ese tipo o utilizar la solución API de reflexión estándar . Puede crear un objeto utilizando el método Activator.CreateInstance .
Si desea llamar a un método genérico, en el uso "normal" se habría inferido su tipo, entonces simplemente se trata de enviar el objeto de tipo desconocido dynamic
. Aquí hay un ejemplo:
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)"
}
}
}
Y aquí está el resultado de este 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
es un método de instancia genérico que escribe el tipo real del argumento pasado (usando el GetType()
método) y el tipo del parámetro genérico (usando el typeof
operador).
Al convertir el argumento del objeto a dynamic
tipo, diferimos proporcionando el parámetro de tipo hasta el tiempo de ejecución. Cuando Process
se llama al método con eldynamic
argumento, al compilador no le importa el tipo de este argumento. El compilador genera código que en tiempo de ejecución comprueba los tipos reales de argumentos pasados (mediante el uso de la reflexión) y elige el mejor método para llamar. Aquí solo existe este método genérico, por lo que se invoca con un parámetro de tipo adecuado.
En este ejemplo, el resultado es el mismo que si escribiera:
foreach (var o in objects)
{
MethodInfo method = typeof(Service).GetMethod("Process");
MethodInfo generic = method.MakeGenericMethod(o.GetType());
generic.Invoke(service, new object[] { o });
}
La versión con un tipo dinámico es definitivamente más corta y más fácil de escribir. Tampoco debe preocuparse por el rendimiento de llamar a esta función varias veces. La próxima llamada con argumentos del mismo tipo debería ser más rápida gracias al mecanismo de almacenamiento en caché en DLR. Por supuesto, puede escribir código que cachee a los delegados invocados, pero al usar el dynamic
tipo obtendrá este comportamiento de forma gratuita.
Si el método genérico al que desea llamar no tiene un argumento de tipo parametrizado (por lo que no se puede inferir su parámetro de tipo), puede ajustar la invocación del método genérico en un método auxiliar como en el siguiente ejemplo:
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) + ">");
}
}
Mayor seguridad de tipo
Lo realmente bueno de usar un dynamic
objeto como reemplazo para usar la API de reflexión es que solo pierde el tiempo de compilación de este tipo en particular que no conoce hasta el tiempo de ejecución. Otros argumentos y el nombre del método son analizados estáticamente por el compilador como de costumbre. Si elimina o agrega más argumentos, cambia sus tipos o cambia el nombre del método, obtendrá un error en tiempo de compilación. Esto no sucederá si proporciona el nombre del método como una cadena Type.GetMethod
y los argumentos como la matriz de objetos MethodInfo.Invoke
.
A continuación se muestra un ejemplo simple que ilustra cómo se pueden detectar algunos errores en tiempo de compilación (código comentado) y otros en tiempo de ejecución. También muestra cómo el DLR intenta resolver qué método llamar.
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);
}
}
Aquí nuevamente ejecutamos algún método lanzando el argumento al dynamic
tipo. Solo la verificación del tipo del primer argumento se pospone al tiempo de ejecución. Obtendrá un error de compilación si el nombre del método al que está llamando no existe o si otros argumentos no son válidos (número incorrecto de argumentos o tipos incorrectos).
Cuando pasa el dynamic
argumento a un método, esta llamada está vinculada últimamente . La resolución de sobrecarga del método ocurre en tiempo de ejecución e intenta elegir la mejor sobrecarga. Entonces, si invoca el ProcessItem
método con un objeto de BarItem
tipo, en realidad llamará al método no genérico, porque es una mejor coincidencia para este tipo. Sin embargo, obtendrá un error de tiempo de ejecución cuando pase un argumento del Alpha
tipo porque no hay ningún método que pueda manejar este objeto (un método genérico tiene la restricción where T : IItem
y la Alpha
clase no implementa esta interfaz). Pero ese es todo el punto. El compilador no tiene información de que esta llamada sea válida. Usted, como programador, sabe esto y debe asegurarse de que este código se ejecute sin errores.
Tipo de retorno gotcha
Cuando se llama a un método no vacío con un parámetro de tipo dinámico, su tipo de retorno probablemente también lo seadynamic
. Entonces, si cambiara el ejemplo anterior a este código:
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
entonces el tipo del objeto resultante sería dynamic
. Esto se debe a que el compilador no siempre sabe qué método se llamará. Si conoce el tipo de retorno de la llamada a la función, debe convertirlo implícitamente al tipo requerido para que el resto del código se escriba estáticamente:
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
Obtendrá un error de tiempo de ejecución si el tipo no coincide.
En realidad, si intenta obtener el valor del resultado en el ejemplo anterior, obtendrá un error de tiempo de ejecución en la segunda iteración del bucle. Esto se debe a que intentó guardar el valor de retorno de una función anulada.