Comments 34
Я может идею не уловил. В чём проблема с Delegate.DynamicInvoke
?
Сеттеры и геттеры можно было собрать и скомпилировать так же как и делагат для методов.
Погодите. Ваша задача что – в рантайме проверить, совпадают ли типы аргументов с требуемыми? Ну в смысле вы заранее не знаете ни тип аргументов делегата (узнаете при его установке), ни тип аргументов, с которыми его вызовут, и просто молитесь, чтобы они совпали?
Почему же "молюсь": тип аргумента подставляется на завершающем этапе работы библиотеки, а представленный механизм позволяет унифициоровать нижестоящий слой, сведя все обработчики к единому виду без необходимости ручного прописывания типов делегатов; иными словами - оперирование конкретными типами всплывает наверх
Это что касается конкретно моего применения
С другой стороны, в чистом виде, мне показалось это весьма неплохим способом абстрактизации и интересным кейсом
Но всё же – если пользователь библиотеки вызвал делегат не с теми аргументами, узнаете вы об этом в рантайме?
И единственная защита – что вызов не выполнится из-за несовпадения типов, т.е. ошибку мы "заметём под ковёр"? Если да, то лучше бы хотя бы сделать как в Delegate.DynamicInvoke – бросить exception.
Сорри, если неправильно вас понял – со смартфона разбираться в коде не очень удобно.
Обработка ошибок кастинга в рантайме предусмотрена, может, она представлена не всеми возможными случаями, но на базовом уровне присутствует, Exception будет выброшен, ниже пример
/// <summary>
/// Реализованный метод вызова анонимного делегата действия
/// </summary>
/// <param name="args">Аргументы действия</param>
/// <returns></returns>
/// <exception cref="DelegateCastingErrorException"></exception>
public override object Invoke(params object[] args)
{
try
{
(_Delegate as Action<T1>).Invoke((T1)args[0]);
return null;
}
catch (Exception e)
{
throw new DelegateCastingErrorException(e.Message);
}
}
И архитектура решительно не даёт возможности перекинуть проверку типов в compile time? Сочувствую.
Может, тогда подумать о кодогенерации – чтобы на обоих концах "трубы", через которую проходит вызов, была одинаковая сигнатура функции? Ну в смысле обвязка вокруг вашего "анонимного" делегата – только для контроля типов?
А использовать dynamic типы автор не пробовал? Там под капотом JIT компиляция... Должно быть быстро...
Увы, автор пробовал, но не запустилось, буду рад, если поделитесь примером кода или ссылкой на код)
У вас в репозитории нет ни одного примера использования ваших классов.
Добавьте в репозитарий тесты производительности и будет более понятно какую задачу решаете и почему dynamic не запускается...
Вы так хотели? https://dotnetfiddle.net/p7i2x1
Ниже выжимка из кода выше
Func<char, int> func = (Func<char, int>) Delegate.CreateDelegate(typeof(Func<char, int>), "Hello", "IndexOf");
dynamic dynFunc = func;
Console.WriteLine("dynFunc(char)");
Console.WriteLine(dynFunc('l'));
Console.WriteLine(dynFunc('o'));
Console.WriteLine(dynFunc('x'));
Console.WriteLine();
Console.WriteLine("dynFunc(dynamic)");
Console.WriteLine(dynFunc((dynamic)'l'));
Console.WriteLine(dynFunc((dynamic)'o'));
Console.WriteLine(dynFunc((dynamic)'x'));
Console.WriteLine();
{
Console.WriteLine("dynFunc(dynObject)");
object o = 'l';
dynamic d = o;
Console.WriteLine(dynFunc(d));
Console.WriteLine();
}
{
// Так нельзя!!! Смотри код выше!!!
Console.WriteLine("dynFunc(object)");
object o = 'l';
Console.WriteLine(dynFunc(o)); // EXCEPTION !!!
Console.WriteLine();
}
Как я писал в нижестоящем ответе (https://habr.com/ru/articles/738346/comments/#comment_25597142), ваш вариант базируется на конкретном контексте, в случае использование "абстрактного" Delegate будет осложнен прямой вызов и, тем более, кастинг передаваемых аргументов
Delegate.CreateDelegate(typeof(Func<char, int>), "Hello", "IndexOf");
создает абстрактный делегат. Вы приведите код где у вас что осложнено?
Переделал код на абстрактный делегат, все работает. https://dotnetfiddle.net/p7i2x1
Ниже выжимка создания абстрактного делегата и оборачивание его в dynamic
:
dynamic dynFunc;
{
MethodInfo method = typeof(string).GetMethod("IndexOf", new Type[] { typeof(char) });
Delegate del = Delegate.CreateDelegate(typeof(Func<char, int>), "Hello", method);
dynFunc = del;
}
Пытаюсь придумать применение этой штуке, и выходит что-то типа редактора GUI-форм, где разные user-control-ы могут выставлять разные свойства, и их нужно унифицированно редактировать/показывать. Но в таком случае чистый reflection более чем достаточен по скорости. А в случаях, где скорость критична, гонять все параметры через object — неправильно.
кастинг дженериков с заменой параметров невозможен.
Возможен, но только в одну сторону. Это называется "контравариантность".
Action<object> foo = x => Console.WriteLine(x);
Action<string> bar = foo;
bar("Hello, world!");
Самый простой способ завернуть вызов метода в какой-нибудь Action<object>
избежав при этом рефлексии это взять MethodInfo
построить по нему Expression<Action<object>>
и сделать ему Compile()
. У меня где-то в исходниках есть кусок такого кода, если на досуге найду, то выложу здесь - там всего дюжины полторы строчек надо для этого.
Вот идея в общих чертах:
using System.Linq.Expressions;
Action<string> foo = x => Console.WriteLine(x);
Action<object> bar = CastAction(foo);
bar("Hello, world!");
static Action<object> CastAction<T>(Action<T> action)
{
// Строим выражение: (act, obj) => act.Invoke((T)obj)
var actParamExpr = Expression.Parameter(typeof(Action<T>));
var objParamExpr = Expression.Parameter(typeof(object));
var convertExpr = Expression.Convert(objParamExpr, typeof(T));
var mi = typeof(Action<T>).GetMethod("Invoke")!;
var invokeExpr = Expression.Call(actParamExpr, mi, convertExpr);
var lambda = Expression.Lambda<Action<Action<T>, object>>(
invokeExpr, actParamExpr, objParamExpr);
var compiled = lambda.Compile();
return obj => compiled(action, obj);
}
У вас тут лишний уровень индирекции вышел, если делать через DynamicMethod
— можно избавиться от замыкания.
Да, можно, конечно, но с System.Reflection.Emit
работать, по-моему, куда сложнее чем с System.Linq.Expressions
.
Не сильно-то и сложнее:
static Action<object> CastAction<T>(Action<T> action)
{
DynamicMethod method = new DynamicMethod(
"CastAndInvoke",
typeof(void),
new[] { typeof(Action<T>), typeof(object) }
);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Unbox_Any, typeof(T));
il.Emit(OpCodes.Callvirt, typeof(Action<T>).GetMethod(nameof(Action.Invoke))!);
il.Emit(OpCodes.Ret);
return method.CreateDelegate<Action<object>>(action);
}
Да, неплохо, возьму на заметку.
Соглашусь, красивый вариант, но он также предполагает указание конкретного типа при конструировании результирующего делегата (см https://habr.com/ru/articles/738346/comments/#comment_25597142)
Доработать для произвольного Delegate его нетрудно, нужно всего-то немного рефлексии для получения типа делегата и типа первого параметра. Моей целью было показать как работать с DynamicMethod.
static Action<object> CastAction(Delegate action)
{
var delegateType = action.GetType();
var invokeMethod = delegateType.GetMethod(nameof(Action.Invoke))!;
if (invokeMethod.GetParameters().Length != 1) throw new ArgumentException();
var parameterType = invokeMethod.GetParameters()[0].ParameterType;
DynamicMethod method = new DynamicMethod(
"CastAndInvoke",
typeof(void),
new[] { delegateType, typeof(object) }
);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Unbox_Any, parameterType);
il.Emit(OpCodes.Callvirt, invokeMethod);
il.Emit(OpCodes.Ret);
return method.CreateDelegate<Action<object>>(action);
}
А не проще тогда в случае вашего примера использовать максимально прострой код
static Action<object> CastAction<T>(Action<T> action)
{
return delegate(object value)
{
action?.Invoke((T)value);
};
}
С другой стороны, в вашем примере метод CastAction<T> предполагает явное указание типа аргумента, которое не потребуется в случае передачи Action<T>, использующее данный тип (контекста будет достаточно). В случае же, если будет осуществлена передача аргумента "абстрактного" типа (Delegate), то контекста для сборки уже не хватит
// Это ваш код
static Action<object> CastActionExpression<T>(Action<T> action)
{
// Строим выражение: (act, obj) => act.Invoke((T)obj)
var actParamExpr = Expression.Parameter(typeof(Action<T>));
var objParamExpr = Expression.Parameter(typeof(object));
var convertExpr = Expression.Convert(objParamExpr, typeof(T));
var mi = typeof(Action<T>).GetMethod("Invoke")!;
var invokeExpr = Expression.Call(actParamExpr, mi, convertExpr);
var lambda = Expression.Lambda<Action<Action<T>, object>>(
invokeExpr, actParamExpr, objParamExpr);
var compiled = lambda.Compile();
return obj => compiled(action, obj);
}
// Эмуляция получения делегата из внешнего источника
static Delegate GetDelegate()
{
Action<int> castingDelegate = delegate (int value)
{
Console.WriteLine(value);
};
return (Delegate)castingDelegate;
}
// Получение объекта делегата
var testDelegate = GetDelegate();
// Тут ошибка с требованием явно указать тип
testDelegate = CastActionExpression(testDelegate);
Если я не прав, то прошу меня поправить, в любом случае, ваш вариант мне очень понравился)
А не проще тогда в случае вашего примера использовать максимально прострой код
Да, но у меня тогда как раз и была такая ситуация как вы уже описали - сам Action<T>
был доступен только в "нетипизированном" виде, поэтому явное приведение (T)value
было невозможно. Я этот кусок кода выдернул сюда просто как пример как можно Expressions
использовать для динамического создания делегатов.
Я вам благодарен за репрезентативный пример)
Я сразу подумал, почему так сложно, а не
static Action<object> CastAction<T>(Action<T> action) => x => action((T)x);
Но если
сам Action[T] был доступен только в "нетипизированном" виде, поэтому явное приведение (T)value было невозможно
То и невозможно
var lambda = Expression.Lambda<Action<Action<T>, object>>(
invokeExpr, actParamExpr, objParamExpr);
Да нет же, там откуда это взято код вообще другой - я его уже тут переделал, чтобы общая мысль была ясна. А в исходном там вообще были даже не делегаты Action<T>
, а generic интерфейсы.
Возможно, просто там будет Expression.Lambda<Action<Delegate, object>>
и первый аргумент тоже надо будет кастовать перед вызовом.
Исходный код вообще вот так вот выглядел:
/// <summary>
/// Creates dispatch delegates.
/// </summary>
public class DispatchBuilder : IDispatchBuilder
{
/// <summary>
/// Builds the dispatch function.
/// </summary>
/// <param name="messageType">Type of the message.</param>
public Func<object, object, CancellationToken, Task> BuildDispatchFunc(Type messageType)
{
// (h, m, ct) => ((IMessageHandler<TMsg>)h).HandleMessageAsync((TMsg)m, ct);
var handlerType = typeof(IMessageHandler<>).MakeGenericType(messageType);
var handleMethod = handlerType.GetMethod(
"HandleMessageAsync", new[] { messageType, typeof(CancellationToken) });
var h = Expression.Parameter(typeof(object));
var m = Expression.Parameter(typeof(object));
var ct = Expression.Parameter(typeof(CancellationToken));
var handler = Expression.Convert(h, handlerType);
var message = Expression.Convert(m, messageType);
var body = Expression.Call(handler, handleMethod, message, ct);
var lambda = Expression.Lambda<Func<object, object, CancellationToken, Task>>(
body, h, m, ct);
return lambda.Compile();
}
}
Но, понятно, что если бы я его тут как есть разместил, то было бы вообще ничего непонятно :))
Да, я немного косо сформулировал свое утверждения, я имел в виду обратное преобразование
Обезличенный вызов делегатов в C#