#c# #.net-core #akka.net
Вопрос:
Хорошо, давайте предположим, что у меня есть такая иерархия классов:
/// In 3rd party library
public class WidgetBase
{
protected void Register<THandler>(Action<THandler> handler) { /* do something */ }
}
public record Message1();
public record Message2();
public sealed class MyWidget : Base
{
public MyClass()
{
RegisterHandlers(this);
}
[Handler]
private void Handle(Message1 msg) {}
[Handler]
private void Handle(Message2 msg) {}
}
public static class Ext
{
// Would prefer extension or normal static method
// and not impose inheritance by putting this
// in an intermediatery base class.
public static void RegisterHandlers<T>(this T t)
{
// Discovers methods with 'Handler' attribute and calls t.Register()
}
}
Таким образом, цель состоит в том, чтобы реализовать RegisterHandlers
, который будет анализировать методы объекта, а затем создавать исполняемый Expression
файл, который вызывает метод регистров базовых классов. Думайте в соответствии с Asp.Net Обработчики основных контроллеров.
Я просто не могу понять, как это сделать. Смысл выражения состоял бы в том, чтобы быть более эффективным, хотя даже решение, основанное на чистом отражении, было бы в порядке.
Я могу обнаружить методы и даже создать выражение, подобное t => this.Handle(t)
, но не могу понять, как вызов универсального метода базового класса выполняется без типа.
В SO есть много похожих вопросов, но я не смог найти точного решения.
[Правка] Сделала пример более ясным.
Комментарии:
1. Являются
SomeType1
иSomeType2
производными отBase
них ?2. «Я могу найти методы и даже создать выражение» можете ли вы опубликовать этот код?
3. Ну, ваш
Register
метод защищен, так что нет никакого способа вызвать его из скомпилированного выражения…4. @canton7 Так как он все равно должен быть специализированным, его все равно нужно будет подтянуть размышлением. В этот момент
protected
это просто руководство.5. IMHO, шаг 1) позвольте C# сделать это и декомпилировать ( sharplab.io/… )
Ответ №1:
Вы можете сделать что-то вроде:
public static void RegisterHandlers<T>(T t) where T : Base
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(Expression.Constant(t), method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(Expression.Constant(t), typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action>(Expression.Block(blockItems)).Compile();
compiled();
}
Смотрите дальше dotnetfiddle.net.
Обратите внимание, что в этом нет особого преимущества по сравнению с простым использованием отражения. Вы не кэшируете созданное compiled
или blockItems
, что является причиной экономии средств при использовании скомпилированных выражений для такого рода вещей.
Однако вы можете немного расширить его и добавить в такой кэш:
private static class Cache<T> where T : Base
{
public static readonly Action<T> Instance = CreateInstance();
private static Action<T> CreateInstance()
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var instanceParameter = Expression.Parameter(typeof(T));
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(instanceParameter, method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(instanceParameter, typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action<T>>(Expression.Block(blockItems), instanceParameter).Compile();
return compiled;
}
}
public static void RegisterHandlers<T>(T t) where T : Base
{
Cache<T>.Instance(t);
}
Смотрите дальше dotnetfiddle.net.
Обратите внимание, как мы теперь используем T
экземпляр в качестве параметра, и это позволяет нам кэшировать созданный Action<T>
.
Комментарии:
1. Хорошо, ваше решение более увлекательное, чем мое. Система.Linq. Выражения-интересное пространство имен, с которым можно поиграть.
2. Я, вероятно, должен был быть более четким в отношении соображений производительности. Это нормально, если регистрация проходит немного медленнее. Он будет вызван в конструкторе класса. Как бы то ни было, зарегистрированный делегат/действие должны иметь такую же производительность, как и обычное действие, поскольку оно будет выполняться много. Контекст-это Акка. Сеть, в которой вы регистрируете обработчики сообщений. Я искал способ удалить некоторые стандартные шаблоны и использовать аналогичный подход, как Asp.Net Ядро делает.
3. Насчет кэширования. Я не уверен, что этот код верен. Вам нужно будет кэшировать каждый экземпляр правильно, так как сгенерированные делегаты привязаны к определенному объекту, что в данном случае противоречит сути. Я думаю, вы могли бы реализовать частичный кэш для части обнаружения, а затем выполнить окончательное связывание и скомпилировать, когда это необходимо.
4. @MarkkuL Тогда не нужно использовать выражения: делегат, созданный с помощью отражения, будет таким же быстрым, как и делегат, созданный с помощью скомпилированного выражения. В кэшировании есть одно скомпилированное выражение для каждого типа , и оно принимает экземпляр в качестве параметра: фактически это просто метод
void E(MyWidget w) { w.Register(Message1 msg => w.Handle(msg); w.Register(Message2 msg => w.Handle(msg)); }
. Обратите внимание , как методE
специализирован на типеMyWidget
, но принимает точный виджетw
в качестве параметра?5. Мне удалось заставить эту версию работать с небольшими изменениями. Фактический метод «регистр» имеет дегларацию,
protected void Receive<T>(Action<T> handler, Predicate<T> shouldHandle = null)
поэтому мне также пришлось предоставить аргумент предиката:c# var nullPred = Expression.Convert( Expression.Constant(null), typeof(Predicate<>).MakeGenericType(messageType));
Ответ №2:
В коде, который у вас есть до сих пор, есть несколько ошибок, в основном связанных с неправильным определением метода расширения. Кроме того, когда вы пишете код, в котором используется несколько разных универсальных типов, которые не все будут принимать один и тот же тип, это помогает дать параметрам типа более описательные имена, чем T
.
Другой момент заключается в том, что ваше расширение, вероятно, должно войти в базовый класс в качестве защищенного метода. Ваш реальный вариант использования может достаточно отличаться от приведенного выше, чтобы гарантировать форму расширения, но подумайте, действительно ли вам это нужно. Является ли расширение тесно связанным с Base
классом? Если это так, то это не очень хороший выбор для расширения, если только базовый класс не находится вне вашего контроля. Даже в этом случае базовый класс прокси может быть лучшим вариантом.
В любом случае, перейдем к прямому решению: размышлению.
Основной поток будет примерно таким:
- Получите универсальный шаблон метода для
Register<>
метода. - Для каждого метода в производном классе, имеющего
Handler
атрибут:- Специализируйте
Register<>
метод с правильным типом параметра. - Создайте правильно введенный делегат для вызова метода в текущем экземпляре.
- Вызовите специализированный метод с делегатом в качестве параметра.
- Специализируйте
- Прибыль.
(Хорошо, я просто предполагаю, что это последнее.)
Самая интересная часть-это получение «правильно введенного делегата» из метода. Хотя MethodInfo
содержит CreateDelegate
метод, вы должны передать правильно специализированный Action<T>
тип. К счастью, тип параметра для вашего специализированного Register<>
метода-это именно то, что нам здесь нужно.
Давайте попробуем простую реализацию:
static class Ext
{
public static void RegisterHandlers<T>(this T instance)
{
// Get the generic method template for `Register<T>`
var registerTemplate = typeof(T).GetMethod("Register", BindingFlags.Instance | BindingFlags.NonPublic);
// Locate all handler methods
var handlerQuery =
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.ReturnType == typeof(void) amp;amp; m.GetCustomAttribute<HandlerAttribute>() != null
let parms = m.GetParameters()
where parms.Length == 1
select (m, parms[0].ParameterType);
foreach (var (handler, parmType) in handlerQuery)
{
// Specialize the Register<> method
var registerMethod = registerTemplate.MakeGenericMethod(parmType);
// Create Action<T> delegate for method
var actionType = registerMethod.GetParameters()[0].ParameterType;
object action = handler.CreateDelegate(actionType, instance);
// Call the specialized Register<> method
registerMethod.Invoke(instance, new[] { action });
}
}
}
Вам нужно будет добавить правильную обработку ошибок и так далее, но это основная идея.
Хотя это работает в данном конкретном случае, я использовал очень простой GetMethod
вызов, чтобы получить шаблон универсального метода. Проверить, что у вас есть правильный метод — что это универсальный шаблон метода (подсказка: IsGenericMethodDefinition
) и что параметр имеет правильный тип — немного сложнее. Общие проблемы могут быть решены таким образом:
var registerTemplate =
(
from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
where m.Name == "Register" amp;amp; m.IsGenericMethodDefinition
let args = m.GetGenericArguments()
where args.Length == 1
let actionTemplate = typeof(Action<>).MakeGenericType(args[0])
let parms = m.GetParameters()
where parms.Length == 1 amp;amp; parms[0].ParameterType == actionTemplate
select m
).FirstOrDefault();
if (registerTemplate is null)
return;
Это, по крайней мере, избавит вас от неприятных сбоев в случае определения несовместимого Register
метода, или неоднозначного совпадения, или… и т. Д.
Как бы это ни было весело, я бы серьезно отнесся к поиску метода без отражения, где вы можете. Отражение может быть немного беспорядочным и довольно медленным. Если вы не можете устранить отражение, по крайней мере, постарайтесь свести его к минимуму. Вы могли бы сделать так, чтобы ваш Register
метод принимал a Delegate
и a Type
вместо общего метода, для которого требуется an Action<T>
.
Комментарии:
1. Вы поднимаете хорошие вопросы об именовании. Должно получиться лучше, даже если это просто образец кода. Просто хочу отметить, что
RegisterHandlers
это не объявлено неправильно, это не метод расширения, это просто статический метод. Статический класс, возможно, назван неверно. Жаль, что C# не поддерживает свободные функции и заставляет помещать все в класс. Базовый класс взят из сторонней библиотеки, поэтому я не могу это контролировать. Одним из вариантов было бы создать подкласс и поместить туда метод, но это вынуждает пользователя использовать этот класс в качестве основы, что не является оптимальным, поскольку C# имеет только одно наследование.