Вызов универсального метода с параметром типа, известным только во время выполнения, может быть значительно упрощен при использовании типа dynamic
вместо API отражения.
Чтобы использовать эту технику, тип должен быть известен из фактического объекта (а не только экземпляра класса Type
). В противном случае вам необходимо создать объект этого типа или использовать стандартное API отражения решение . Вы можете создать объект, используя метод Activator.CreateInstance .
Если вы хотите вызвать универсальный метод, который при «нормальном» использовании имел бы тип его логического вывода, то это просто сводится к приведению объекта неизвестного типа к dynamic
. Вот пример:
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)"
}
}
}
А вот и вывод этой программы:
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
- это универсальный метод экземпляра, который записывает реальный тип переданного аргумента (с помощью метода GetType()
) и тип универсального параметра (с помощью оператора typeof
).
Приведя аргумент объекта к типу dynamic
, мы отложили предоставление параметра типа до времени выполнения. Когда метод Process
вызывается с аргументом dynamic
, компилятору не важен тип этого аргумента. Компилятор генерирует код, который во время выполнения проверяет реальные типы передаваемых аргументов (используя отражение) и выбирает лучший метод для вызова. Здесь есть только один универсальный метод, поэтому он вызывается с правильным параметром типа.
В этом примере вывод такой же, как если бы вы написали:
foreach (var o in objects)
{
MethodInfo method = typeof(Service).GetMethod("Process");
MethodInfo generic = method.MakeGenericMethod(o.GetType());
generic.Invoke(service, new object[] { o });
}
Версия с динамическим типом определенно короче и ее легче написать. Вы также не должны беспокоиться о производительности вызова этой функции несколько раз. Следующий вызов с аргументами того же типа должен быть быстрее благодаря механизму caching в DLR. Конечно, вы можете написать код, который кэширует вызываемых делегатов, но, используя тип dynamic
, вы получаете это поведение бесплатно.
Если универсальный метод, который вы хотите вызвать, не имеет аргумента параметризованного типа (поэтому его параметр типа не может быть выведен), вы можете заключить вызов универсального метода в вспомогательный метод, как показано ниже пример:
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) + ">");
}
}
Повышенная безопасность типов
Что действительно хорошо в использовании dynamic
объекта в качестве замены для использования API отражения, так это то, что вы теряете только проверку времени компиляции этого конкретного типа, которую вы не знаете до времени выполнения. Другие аргументы и имя метода статически анализируются компилятором как обычно. Если вы удалите или добавите больше аргументов, измените их типы или переименуете имя метода, вы получите ошибку времени компиляции. Этого не произойдет, если вы предоставите имя метода в виде строки в Type.GetMethod
и аргументы в виде массива объектов в MethodInfo.Invoke
.
Ниже приведен простой пример, который иллюстрирует, как некоторые ошибки могут быть обнаружены во время компиляции (закомментированный код), а другие - во время выполнения. Также показано, как DLR пытается определить, какой метод вызывать.
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);
}
}
Здесь мы снова выполняем некоторый метод, приводя аргумент к типу dynamic
. Только проверка типа первого аргумента откладывается до времени выполнения. Вы получите ошибку компилятора, если имя метода, который вы вызываете, не существует или если другие аргументы недопустимы (неправильное количество аргументов или неправильные типы).
Когда вы передаете аргумент dynamic
методу, этот вызов в последнее время связан . Разрешение перегрузки метода происходит во время выполнения и пытается выбрать наилучшую перегрузку. Так что если вы вызываете метод ProcessItem
с объектом типа BarItem
, то вы фактически вызовете неуниверсальный метод, потому что он лучше подходит для этого типа. Однако вы получите ошибку во время выполнения при передаче аргумента типа Alpha
, потому что нет метода, который может обработать этот объект (универсальный метод имеет ограничение where T : IItem
, а класс Alpha
не реализует этот интерфейс ). Но в этом все дело. Компилятор не имеет информации, что этот вызов действителен. Вы, как программист, знаете это, и вы должны убедиться, что этот код выполняется без ошибок.
Возвращаемый тип gotcha
Когда вы вызываете не void метод с параметром динамического типа, его тип возвращаемого значения, вероятно, будет тоже dynamic
. Так что если вы измените предыдущий пример на этот код:
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
тогда тип объекта результата будет dynamic
. Это потому, что компилятор не всегда знает, какой метод будет вызван. Если вы знаете тип возвращаемого значения вызова функции, тогда вам следует неявно преобразовать в требуемый тип, чтобы остальная часть кода была статически напечатана:
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
Вы получите ошибку во время выполнения, если тип не соответствует.
На самом деле, если вы попытаетесь получить значение результата в предыдущем примере, вы получите ошибку времени выполнения во второй итерации цикла. Это потому, что вы пытались сохранить возвращаемое значение функции void.