Создайте исполняемое выражение, вызывающее универсальный метод

#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# имеет только одно наследование.