Как использовать рефлексию для вызова универсального метода?
Каков наилучший способ вызова универсального метода, когда параметр типа неизвестен во время компиляции, а вместо этого получается динамически во время выполнения?
Рассмотрим следующий пример кода - внутри Example()
метод, какой самый краткий способ вызвать GenericMethod<T>()
с использованием Type
хранится в myType
переменная?
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>()
{
//...
}
}
9 ответов
Вам нужно использовать отражение, чтобы получить метод для запуска, а затем "создать" его, предоставив аргументы типа с помощью MakeGenericMethod:
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
Для статического метода передайте null
в качестве первого аргумента Invoke
, Это не имеет ничего общего с общими методами - это просто нормальное отражение.
Как уже отмечалось, многое из этого проще с использованием C# 4 dynamic
- если вы можете использовать вывод типа, конечно. Это не помогает в случаях, когда вывод типа недоступен, например, точный пример в вопросе.
Просто дополнение к оригинальному ответу. Пока это будет работать
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
Это также немного опасно, потому что вы теряете проверку во время компиляции для GenericMethod
, Если вы позже сделаете рефакторинг и переименуете GenericMethod
, этот код не заметит и потерпит неудачу во время выполнения. Также, если есть какая-либо постобработка сборки (например, запутывание или удаление неиспользуемых методов / классов), этот код может также сломаться.
Итак, если вы знаете метод, на который вы ссылаетесь во время компиляции, и он не вызывается миллионы раз, поэтому накладные расходы не имеют значения, я бы изменил этот код на:
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);
Хотя это не очень красиво, у вас есть ссылка на время компиляции GenericMethod
здесь, и если вы рефакторинг, удалить или сделать что-нибудь с GenericMethod
, этот код будет продолжать работать или, по крайней мере, сломаться во время компиляции (если, например, вы удалите GenericMethod
).
Другой способ сделать то же самое - создать новый класс-обертку и создать его через Activator
, Я не знаю, есть ли лучший способ.
Вызов универсального метода с параметром типа, известным только во время выполнения, может быть значительно упрощен с помощью 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 });
}
Версия с динамическим типом определенно короче и проще для написания. Вы также не должны беспокоиться о производительности вызова этой функции несколько раз. Следующий вызов с аргументами того же типа должен быть быстрее благодаря механизму кэширования в 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
type, тогда вы на самом деле вызовете неуниверсальный метод, потому что он лучше подходит для этого типа. Тем не менее, вы получите ошибку во время выполнения при передаче аргумента Alpha
тип, потому что нет метода, который может обрабатывать этот объект (универсальный метод имеет ограничение where T : IItem
а также Alpha
класс не реализует этот интерфейс). Но в этом все дело. Компилятор не имеет информации, что этот вызов действителен. Вы, как программист, знаете это, и вы должны убедиться, что этот код выполняется без ошибок.
Возвращаемый тип
Когда вы вызываете не void метод с параметром динамического типа, его тип возвращаемого значения, вероятно, будетdynamic
тоже. Так что если вы измените предыдущий пример на этот код:
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
тогда тип объекта результата будет dynamic
, Это потому, что компилятор не всегда знает, какой метод будет вызван. Если вам известен тип возвращаемого значения вызова функции, вам следует неявно преобразовать его в требуемый тип, чтобы остальная часть кода была статически типизирована:
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
Вы получите ошибку во время выполнения, если тип не соответствует.
На самом деле, если вы попытаетесь получить значение результата в предыдущем примере, вы получите ошибку времени выполнения во второй итерации цикла. Это потому, что вы пытались сохранить возвращаемое значение функции void.
Добавляем к ответу Адриана Галлеро:
Вызов универсального метода из информации о типе включает три шага.
TLDR: вызов известного универсального метода с объектом типа может быть выполнен с помощью:
((Action)GenericMethod<object>)
.Method
.GetGenericMethodDefinition()
.MakeGenericMethod(typeof(string))
.Invoke(this, null);
где GenericMethod<object>
является именем метода для вызова и любого типа, который удовлетворяет общим ограничениям.
(Действие) соответствует сигнатуре вызываемого метода т.е.Func<string,string,int>
или же Action<bool>
)
Шаг 1 - получение MethodInfo для определения общего метода
Метод 1: Используйте GetMethod() или GetMethods() с соответствующими типами или флагами привязки.
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
Метод 2: Создайте делегат, получите объект MethodInfo и затем вызовите GetGenericMethodDefinition
Внутри класса, который содержит методы:
MethodInfo method = ((Action)GenericMethod<object>)
.Method
.GetGenericMethodDefinition();
MethodInfo method = ((Action)StaticMethod<object>)
.Method
.GetGenericMethodDefinition();
Из-за пределов класса, который содержит методы:
MethodInfo method = ((Action)(new Sample())
.GenericMethod<object>)
.Method
.GetGenericMethodDefinition();
MethodInfo method = ((Action)Sample.StaticMethod<object>)
.Method
.GetGenericMethodDefinition();
В C# имя метода, то есть "ToString" или "GenericMethod", фактически относится к группе методов, которая может содержать один или несколько методов. Пока вы не укажете типы параметров метода, неизвестно, на какой метод вы ссылаетесь.
((Action)GenericMethod<object>)
ссылается на делегата для конкретного метода. ((Func<string, int>)GenericMethod<object>)
ссылается на другую перегрузку GenericMethod
Метод 3: Создайте лямбда-выражение, содержащее выражение вызова метода, получите объект MethodInfo и затем GetGenericMethodDefinition
MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
(Sample v) => v.GenericMethod<object>()
)).Body).Method.GetGenericMethodDefinition();
Это разбивается на
Создайте лямбда-выражение, где тело - это вызов нужного вам метода.
Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();
Извлеките тело и приведите к MethodCallExpression
MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;
Получить определение общего метода из метода
MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();
Шаг 2 вызывает MakeGenericMethod для создания универсального метода с соответствующими типами.
MethodInfo generic = method.MakeGenericMethod(myType);
Шаг 3 вызывает метод с соответствующими аргументами.
generic.Invoke(this, null);
В C# 4.0 отражение не требуется, поскольку DLR может вызывать его, используя типы времени выполнения. Поскольку использование библиотеки DLR динамически затрудняет работу (вместо того, чтобы компилятор генерировал для вас код C#), среда с открытым исходным кодом Dynamitey (.net стандарт 1.5) предоставляет вам простой кэшированный доступ во время выполнения к тем же вызовам, которые генерирует компилятор. для тебя.
var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));
var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));
Никто не предоставил "классическое отражение", так что вот полный пример кода:
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;
}
}
}
Выше DynamicDictionaryFactory
у класса есть метод
CreateDynamicGenericInstance(Type keyType, Type valueType)
и он создает и возвращает экземпляр IDictionary, типы ключей и значений которого точно указаны в вызове keyType
а также valueType
,
Вот полный пример того, как вызвать этот метод для создания экземпляра и использования 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");
}
}
}
Когда приведенное выше консольное приложение выполняется, мы получаем правильный ожидаемый результат:
Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3
Это мои 2 цента, основанные на ответе Grax, но с двумя параметрами, необходимыми для универсального метода.
Предположим, ваш метод определен следующим образом в классе Helpers:
public class Helpers
{
public static U ConvertCsvDataToCollection<U, T>(string csvData)
where U : ObservableCollection<T>
{
//transform code here
}
}
В моем случае тип U всегда является наблюдаемым объектом хранения коллекции типа T.
Поскольку у меня есть предопределенные типы, я сначала создаю "фиктивные" объекты, которые представляют наблюдаемую коллекцию (U) и объект, сохраненный в ней (T), и которые будут использоваться ниже, чтобы получить их тип при вызове Make
object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);
Затем вызовите GetMethod, чтобы найти вашу универсальную функцию:
MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");
Пока что вышеупомянутый вызов в значительной степени идентичен тому, что было объяснено выше, но с небольшой разницей, когда вам нужно передать ему несколько параметров.
Вам необходимо передать массив Type[] в функцию MakeGenericMethod, которая содержит типы "фиктивных" объектов, которые были созданы выше:
MethodInfo generic = method.MakeGenericMethod(
new Type[] {
myCollection.GetType(),
myObject.GetType()
});
Как только это будет сделано, вам нужно вызвать метод Invoke, как указано выше.
generic.Invoke(null, new object[] { csvData });
И вы сделали. Работает шарм!
ОБНОВИТЬ:
Как подчеркнул @Bevan, мне не нужно создавать массив при вызове функции MakeGenericMethod, поскольку она принимает параметры, и мне не нужно создавать объект для получения типов, поскольку я могу просто передавать типы непосредственно в эту функцию. В моем случае, так как у меня есть типы, предопределенные в другом классе, я просто изменил свой код на:
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 содержит 2 свойства типа Type
который я устанавливаю во время выполнения на основе значения enum, переданного конструктору, и предоставит мне соответствующие типы, которые я затем использую в MakeGenericMethod.
Еще раз спасибо за выделение этого @Bevan.
Вдохновленный ответом Enigmativity - предположим, у вас есть два (или более) класса, например
public class Bar { }
public class Square { }
и вы хотите вызвать метод Foo<T>
с Bar
а также Square
, который объявлен как
public class myClass
{
public void Foo<T>(T item)
{
Console.WriteLine(typeof(T).Name);
}
}
Затем вы можете реализовать метод расширения, например:
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 });
}
}
При этом вы можете просто вызвать Foo
нравиться:
var objSquare = new Square();
objSquare.InvokeFoo();
var objBar = new Bar();
objBar.InvokeFoo();
который работает для каждого класса. В этом случае он выведет:
Квадратный
бар
Несмотря на то, что это довольно старый вопрос, он показался мне интересным, поскольку существует несколько вариантов динамического вызова метода. Буквально это отражение, деревья выражений и излучатель. Исторически сложилось так, что отражение является самым медленным вариантом, а излучатель — самым быстрым. Поэтому я решил сравнить их в этом интригующем деле и разобраться, есть ли какие-то изменения в наши дни. Исходный вопрос требует ** наилучшего способа вызова универсального метода, когда параметр типа неизвестен во время компиляции**. Однако почти во всех ответах выше предлагалось использовать отражение.
Я создал три тестовых примера для всех упомянутых подходов. Во-первых, вот слегка измененный образец класса, который будет протестирован с помощью трех методов: TestReflection, TestExpression и TestEmit.
public class Sample
{
public void TestDirectCall(Type type)
{
GenericMethod<string>();
GenericMethodWithArg<string>(42);
StaticMethod<string>();
StaticMethodWithArg<string>(6);
}
public void TestReflection(Type type)
{
CallViaReflection.CallGenericMethod(this, type);
CallViaReflection.CallGenericMethod(this, type, 42);
CallViaReflection.CallStaticMethod(type);
CallViaReflection.CallStaticMethod(type, 6);
}
public void TestExpression(Type type)
{
CallViaExpression.CallGenericMethod(this, type);
CallViaExpression.CallGenericMethod(this, type, 42);
CallViaExpression.CallStaticMethod(type);
CallViaExpression.CallStaticMethod(type, 6);
}
public void TestEmit(Type type)
{
CallViaEmit.CallGenericMethod(this, type);
CallViaEmit.CallGenericMethod(this, type, 42);
CallViaEmit.CallStaticMethod(type);
CallViaEmit.CallStaticMethod(type, 6);
}
public void T()
{
StaticMethod<string>();
}
public void GenericMethod<T>()
{
}
public void GenericMethodWithArg<T>(int someValue)
{
}
public static void StaticMethod<T>()
{
}
public static void StaticMethodWithArg<T>(int someValue)
{
}
}
Класс CallViaReflection представляет собой вспомогательный класс, который выполняет вызовы универсальных методов посредством отражения. Я решил ввести кэш для лучших результатов.
public static class CallViaReflection
{
private readonly static Cache<MethodInfo> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
callDelegate.Invoke(sample, null);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(sample, new object[] { someValue });
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
callDelegate.Invoke(null, null);
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(null, new object[] { someValue });
}
private static MethodInfo GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
return concreteMethodInfo;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
cache.Add(methodName, genericType, concreteMethodInfo);
return concreteMethodInfo;
}
}
Следующий класс CallViaExpression использует кэшированные деревья выражений.
public static class CallViaExpression
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentExpr = arguments.Select((type, i) => Expression.Parameter(type, "arg" + i)).ToArray();
if (concreteMethodInfo.IsStatic)
{
var callExpr = Expression.Call(concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, argumentExpr).Compile();
}
else
{
var parameterExpr = Expression.Parameter(sampleType, "sample");
var callExpr = Expression.Call(parameterExpr, concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, new[] { parameterExpr }.Union(argumentExpr).ToArray()).Compile();
}
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
}
И последнее, но не менее важное: CallViaEmit генерирует необходимые операции.
public static class CallViaEmit
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(this Sample sample, Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(this Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType);
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDynamicMethod(string methodName, BindingFlags bindingFlags, Type genericType)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var genericMethodInfo = typeof(Sample).GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentTypes = concreteMethodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); ;
var dynamicMethodArgs = concreteMethodInfo.IsStatic
? argumentTypes
: new[] { typeof(Sample) }.Union(argumentTypes).ToArray();
var dynamicMethod = new DynamicMethod("DynamicCall", null, dynamicMethodArgs);
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Nop);
switch (dynamicMethodArgs.Length)
{
case 0:
break;
case 1:
il.Emit(OpCodes.Ldarg_0);
break;
case 2:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
break;
case 3:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
break;
default:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldarg_3);
for (int i = 4; i < argumentTypes.Length; i++)
{
il.Emit(OpCodes.Ldarg, argumentTypes[i]);
}
break;
}
il.EmitCall(concreteMethodInfo.IsStatic ? OpCodes.Call : OpCodes.Callvirt, concreteMethodInfo, null);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret);
callDelegate = dynamicMethod.CreateDelegate(GetActionType(dynamicMethodArgs));
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
private static Type GetActionType(Type[] argumentTypes)
{
switch (argumentTypes.Length)
{
case 0:
return typeof(Action);
case 1:
return typeof(Action<>).MakeGenericType(argumentTypes);
case 2:
return typeof(Action<,>).MakeGenericType(argumentTypes);
case 3:
return typeof(Action<,,>).MakeGenericType(argumentTypes);
case 4:
return typeof(Action<,,,>).MakeGenericType(argumentTypes);
case 5:
return typeof(Action<,,,,>).MakeGenericType(argumentTypes);
case 6:
return typeof(Action<,,,,,>).MakeGenericType(argumentTypes);
case 7:
return typeof(Action<,,,,,,>).MakeGenericType(argumentTypes);
case 8:
return typeof(Action<,,,,,,,>).MakeGenericType(argumentTypes);
default:
throw new NotSupportedException("Action with more than 8 arguments is not supported");
}
}
}
Наконец, вот тестовый класс и результаты тестов.
[TestFixture]
public class SampleTests
{
private const int Iterations = 10000000;
[Test]
public void TestDirectCall()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestDirectCall(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods directly took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestReflection()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestReflection(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via reflection took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestExpressionTree()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestExpression(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via expression tree took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestEmit()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestEmit(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via emit took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
}
Динамический вызов методов через Emit занял 2939 миллисекунд. Динамический вызов методов через дерево выражений занял 3910 миллисекунд. Динамический вызов методов посредством отражения занял 6381 миллисекунду.
Очевидно, что победителем становится эмитент. Он по-прежнему работает более чем в два раза быстрее. Второе место занимают деревья выражений.
Поэтому мой вердикт не меняется уже второе десятилетие. Если вам нужно вызвать метод динамически, начните с деревьев выражений. Если ваш код критичен к производительности, используйте ILGenerator и создайте необходимые вызовы. Несмотря на то, что на первый взгляд это может показаться сложным, все необходимые инструкции MSIL можно легко дизассемблировать с помощью ildasm.
Исходный код доступен на GitHub .