В чем, собственно, дело?

Как говорится: сидел, никого не трогал, программировал на C#, и тут родилась «хитрая» нужда. В рамках одного из своих проектов мне понадобился механизм обезличенного вызова делегатов, который бы позволил организовать их универсальные хранение и вызов. Также, главной целью разработки являлся уход от необходимости вызова методов конкретных экземпляров объектов через рефлексию (рефлексия используется только на этапе инициализации), что в конечном счете сильно увеличило производительность.

Обезличенный вызов делегата - вызов делегата с известным количеством параметров, но с неизвестными типами параметров, где каждый тип параметра представлен базовым классом Object.

Поискав готовое решение в интернете, не нашел ничего, кроме упоминаний, что так делать нельзя, или что кастинг дженериков с заменой параметров невозможен. Действительно, если попробовать сделать это, то будет получена ошибка во время выполнения программы.

Можно сказать, что подобные результаты меня не удовлетворяли в высшей степени, поэтому я принялся за изобретение нового «велосипеда», немного углубившись в тему рефлексии.

Да-да, я рефлексировал над рефлексией
Да-да, я рефлексировал над рефлексией

Перебрав все возможные способы преобразовать один тип к другому, и убедившись, что это невозможно (если возможно, то я не виноват), я пришел к дивной идее, что можно использовать вспомогательный объект-дженерик с идентичной сигнатурой типов, который обернет обезличенный объект делегата, подставив нужные тип на свои места.

Не могу передать количество радости и самоудовлетворения, которое я испытал, когда от шальной идеи я перешел к рабочему результату. От шуток, собственно, переходим к делу, сейчас опишу, что было сделано.

Основной изюм подхода

Корнем решения являются классы-обертки, наследуемые от абстрактного класса DelegateCaster, который предоставляет метод для анонимного вызова делегата.

/// <summary>
/// Базовый класс кастера
/// </summary>
public abstract class DelegateCaster
{
    /// <summary>
    /// Объект обрабатываесого делагата
    /// </summary>
    protected Delegate _Delegate { get; } = null;
    /// <summary>
    /// Конструктор объекта кастера
    /// </summary>
    /// <param name="delegateObject"></param>
    internal DelegateCaster(Delegate delegateObject)
    {
        _Delegate = delegateObject;
    }
    /// <summary>
    /// Метод анонимного вызова делегата
    /// </summary>
    /// <param name="args"></param>
    /// <returns></returns>
    public abstract object Invoke(params object[] args);
}

Классы-обертка представляют из себя дженерики, реализуемые на идентичном по отношению к оборачиваемому делегату наборе аргументов типа. По своей сути, данные классы становится фабрикой по производству обработчиков анонимных вызовов делегатов. Предлагаю рассмотреть описанный выше механизм на примере анонимного кастера от трех аргументов типа.

/// <summary>
/// Класс для кастинга делегатов
/// </summary>
internal abstract class DelegateCaster3Arg<T1, T2, T3> : DelegateCaster
{
    /// <summary>
    /// Конструктор объекта
    /// </summary>
    /// <param name="delegateObject"></param>
    internal DelegateCaster3Arg(Delegate delegateObject) : base(delegateObject)
    {
    }
    /// <summary>
    /// Получить анонимный делегат действия
    /// </summary>
    /// <param name="actionDelegate"></param>
    /// <returns></returns>
    public static Action<object, object, object> _GetObjectiveAction(object actionDelegate)
    {
        if (actionDelegate is Action<T1, T2, T3>)
        {
            return delegate (object p1, object p2, object p3)
            {
                (actionDelegate as Action<T1, T2, T3>)?.Invoke((T1)p1, (T2)p2, (T3)p3);
            };
        }
        else
        {
            return null;
        }
    }
    /// <summary>
    /// Получить анонимный делегат функции
    /// </summary>
    /// <param name="functionDelegate"></param>
    /// <returns></returns>
    public static Func<object, object, object> _GetObjectiveFunction(object functionDelegate)
    {
        if (functionDelegate is Func<T1, T2, T3>)
        {
            return delegate (object p1, object p2)
            {
                return (functionDelegate as Func<T1, T2, T3>).Invoke((T1)p1, (T2)p2);
            };
        }
        else
        {
            return null;
        }
    }
    /// <summary>
    /// Получить объект кастера делегата действия
    /// </summary>
    /// <param name="delegateObject"></param>
    /// <returns></returns>
    public static DelegateCaster _GetActionCaster(Delegate delegateObject)
    {
        if (delegateObject is Action<T1, T2, T3>)
        {
            return new ActionCaster3Arg<T1, T2, T3>(delegateObject);
        }
        return null;
    }
    /// <summary>
    /// Получить объект кастера делегата функции
    /// </summary>
    /// <param name="delegateObject"></param>
    /// <returns></returns>
    public static DelegateCaster _GetFunctionCaster(Delegate delegateObject)
    {
        if (delegateObject is Func<T1, T2, T3>)
        {
            return new FunctionCaster3Arg<T1, T2, T3>(delegateObject);
        }
        return null;
    }
}

Основной изюминкой представленного подхода является приведение типов Object параметров анонимного делегата к параметрам реального делегата, указанных в сигнатуре объекта кастера.

// Результирующий анонимный делегат
return delegate (object p1, object p2, object p3)
{
    // Примедение типов в исходном конкретном делегате
    (actionDelegate as Action<T1, T2, T3>)?.Invoke((T1)p1, (T2)p2, (T3)p3);
};

В свою очередь, данный класс наследуется соответствующими классами анонимной функции (FunctionCaster) и анонимного действия (ActionCaster), которые реализуют абстрактный метод Invoke, объявленный в родительском классе DelegateCaster.

Пример реализации класса-обработчика вызова анонимного действия для трех аргументов типа.

/// <summary>
/// Класс кастинга анонимного делегата действия
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="T3"></typeparam>
internal class ActionCaster3Arg<T1, T2, T3> : DelegateCaster3Arg<T1, T2, T3>
{
    internal ActionCaster3Arg(Delegate delegateObject) : base(delegateObject)
    {
    }
    /// <summary>
    /// Реализованный метод вызова анонимного делегата действия
    /// </summary>
    /// <param name="args">Аргументы действия</param>
    /// <returns></returns>
    /// <exception cref="DelegateCastingErrorException"></exception>
    public override object Invoke(params object[] args)
    {
        try
        {
            (_Delegate as Action<T1, T2, T3>).Invoke((T1)args[0], (T2)args[1], (T3)args[2]);

            return null;
        }
        catch (Exception e)
        {
            throw new DelegateCastingErrorException(e.Message);
        }
    }
}

Пример реализации класса-обработчика вызова анонимной функции для трех аргументов типа.

/// <summary>
/// Класс кастинга анонимного делегата функции
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="T3"></typeparam>
internal class FunctionCaster3Arg<T1, T2, T3> : DelegateCaster3Arg<T1, T2, T3>
{
    internal FunctionCaster3Arg(Delegate delegateObject) : base(delegateObject)
    {
    }
    /// <summary>
    /// Реализованный метод вызова анонимного делегата функции
    /// </summary>
    /// <param name="args">Аргементы функции</param>
    /// <returns></returns>
    /// <exception cref="DelegateCastingErrorException"></exception>
    public override object Invoke(params object[] args)
    {
        try
        {
            return (_Delegate as Func<T1, T2, T3>).Invoke((T1)args[0], (T2)args[1]);
        }
        catch (Exception e)
        {
            throw new DelegateCastingErrorException(e.Message);
        }
    }
}

Сведение к единому обработчику

На данном этапе можно сказать, что основные нюансы закончились, и осталось только доработать процесс автоматического выбора корректного класса-обертки для создания экземпляра анонимного делегата. Для этих целей базовый класс DelegateCaster был расширен набором статических методов.

/// <summary>
/// Получить объект приведенного делегата к типам Action<...object...> или Func<...object...>
/// </summary>
/// <param name="delegateObject"></param>
/// <returns></returns>
public static object GetObjective(object delegateObject)
{
    try
    {
        // Получаем тип делегата
        Type delegateObjectType = delegateObject.GetType();
        // Получаем коректный тип объекта-обертки (фабрики)
        Type casterType = _GetCasterType(delegateObjectType);
        
        if (casterType is null) return null;
        // Если делегат явялется действием
        if (delegateObjectType.Name.Contains("Action"))
        {
            // С помощью рефлексии создаем анонимный делегат действия
            return casterType.GetMethod("_GetObjectiveAction").Invoke(null, new object[] { delegateObject });
        }
        // Если делегат явялется функцией
        else
        {
            // С помощью рефлексии создаем анонимный делегат функции
            return casterType.GetMethod("_GetObjectiveFunction").Invoke(null, new object[] { delegateObject });
        }
    }
    catch (Exception e)
    {
        throw new DelegateCasterCreationErrorException(e.Message);
    }
}
/// <summary>
/// Получить анонимый делегат для вызова посредствам метода Invoke(params object args[])
/// </summary>
/// <param name="delegateObject"></param>
/// <returns></returns>
public static DelegateCaster GetCaster(object delegateObject)
{
    try
    {
        // Получаем тип делегата
        Type delegateObjectType = delegateObject.GetType();
        // Получаем коректный тип объекта-обертки
        Type casterType = _GetCasterType(delegateObjectType);

        if (casterType is null) return null;
        // Если делегат явялется действием
        if (delegateObjectType.Name.Contains("Action"))
        {
            // С помощью рефлексии создаем конктетный обработчик делегата действия
            return casterType.GetMethod("_GetActionCaster")?.Invoke(null, new object[] { delegateObject }) as DelegateCaster;
        }
        // Если делегат явялется функцией
        else
        {
            // С помощью рефлексии создаем конктетный обработчик делегата функции
            return casterType.GetMethod("_GetFunctionCaster")?.Invoke(null, new object[] { delegateObject }) as DelegateCaster;
        }
    }
    catch (Exception e)
    {
        throw new DelegateCasterCreationErrorException(e.Message);
    }
}

С целью упростить навигацию между классами был создан статический словарь соответствующих типов. Таким образом, при расширении набора классов-оберток необходимо будет только добавить их в словарь при инициализации.

/// <summary>
/// Статический конструктор кастера
/// </summary>
static DelegateCaster()
{
    // Тип для кастинга делегатов от 1-ого аргумента типа
    _CastingTypes.Add(typeof(Action).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster0Arg"));
    
    // Тип для кастинга делегатов от 2-х аргументов типа
    _CastingTypes.Add(typeof(Action<int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster1Arg`1"));
    _CastingTypes.Add(typeof(Func<int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster1Arg`1"));
    
    // Тип для кастинга делегатов от 2-х аргументов типа
    _CastingTypes.Add(typeof(Action<int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster2Arg`2"));
    _CastingTypes.Add(typeof(Func<int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster2Arg`2"));
    
    // Тип для кастинга делегатов от 3-х аргументов типа
    _CastingTypes.Add(typeof(Action<int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster3Arg`3"));
    _CastingTypes.Add(typeof(Func<int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster3Arg`3"));
    
    // Тип для кастинга делегатов от 4-х аргументов типа
    _CastingTypes.Add(typeof(Action<int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster4Arg`4"));
    _CastingTypes.Add(typeof(Func<int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster4Arg`4"));
    
    // Тип для кастинга делегатов от 5-и аргументов типа
    _CastingTypes.Add(typeof(Action<int, int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster5Arg`5"));
    _CastingTypes.Add(typeof(Func<int, int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster5Arg`5"));
    
    // Тип для кастинга делегатов от 6-и аргументов типа
    _CastingTypes.Add(typeof(Action<int, int, int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster6Arg`6"));
    _CastingTypes.Add(typeof(Func<int, int, int, int, int, int>).Name, Type.GetType("DelegatesCastingLib.Casters.DelegateCaster6Arg`6"));
}

Фактически, все вышеописанные манипуляции с кодом были необходимы для реализации возможности создания "правильного" экземпляра типа объекта-обертки.

/// <summary>
/// Получить тип кастера по типу делегата
/// </summary>
/// <param name="delegateType"></param>
/// <returns></returns>
private static Type _GetCasterType(Type delegateType)
{
    // Проверка, что данный тип зарегистрирован в словаре
    if (_CastingTypes.ContainsKey(delegateType.Name))
    {
        // Создание конкретного экземпляра типа с подстановкой "правильных" аргумента типа
        return _CastingTypes[delegateType.Name]?.MakeGenericType(delegateType.GenericTypeArguments);
    }
    return null;
}

Бонус

В качестве необходимого бонуса был реализован механизм извлечения метода объекта в качестве самостоятельного делегата.

/// <summary>
/// Метод для получения делегата метода объекта
/// </summary>
/// <param name="ownerType">Тип объекта-владельца метода</param>
/// <param name="methodInfo">Металанные метода</param>
/// <returns></returns>
public static Delegate GetMethodDelegate(Type ownerType, MethodInfo methodInfo)
{
    try
    {
        // Получаем аргумент вызываемого типа объекта-владельца
        var instance = Expression.Parameter(ownerType);

        // Получаем список аргументов вызываемого метода
        var methodParams = methodInfo.GetParameters().Select((p) => Expression.Parameter(p.ParameterType)).ToList();

        // Получаем непосредственно вызываемый метод
        var call = Expression.Call(instance, methodInfo, methodParams);

        // Вставляем на первую позицию аргмент типа объекта-владельца
        methodParams.Insert(0, instance);

        // "Запекаем" полученный метод для получения делегата
        return Expression.Lambda(call, methodParams).Compile();
    }
    catch
    {
        return null;
    }
}

Пример применения предложенного метода

В качестве примера приведена генерация обертки абстрактного свойства (собственно, это кусок проекта, над которым я работал).

/// <summary>
/// Конструктор абстрактного свойства
/// </summary>
public AbstractProperty(Type ownerType, PropertyInfo property, string name)
{
    Name = name;
    Type = property.PropertyType;

    _SetMethod = property.SetMethod;

    // Инициализация делегата установки значения свойства
    _IsSetMethodAvailable = _SetMethod != null && _SetMethod.IsPublic && _SetMethod.GetParameters().Length == 1;
    if (IsSetMethodAvailable)
    {
        _SetMethodDelegate = (Action<object, object>)DelegateCaster.GetObjectiveAction(ReflectionMethodExtractor.GetMethodDelegate(ownerType, _SetMethod));
    }

    // Инициализация делегата получения значения свойства
    _GetMethod = property.GetMethod;

    _IsGetMethodAvailable = _GetMethod != null && _GetMethod.IsPublic && _GetMethod.GetParameters().Length == 0;
    if (IsGetMethodAvailable)
    {
        _GetMethodDelegate = (Func<object, object>)DelegateCaster.GetObjectiveFunction(ReflectionMethodExtractor.GetMethodDelegate(ownerType, _GetMethod));
    }
}

Послесловие

За возможностью ознакомиться с наработками можно постучаться на git по приложенной ссылке, где мной был реализован джентельменский набор для делегатов вплоть до 6 аргументов (Action и Func): при необходимости можно масштабировать до требуемой размерности.

Комментарии (34)


  1. Ordos
    29.05.2023 15:22
    +2

    Я может идею не уловил. В чём проблема с Delegate.DynamicInvoke?


    1. DemiEljar Автор
      29.05.2023 15:22
      +1

      Проблема в скорости работы: по моим тестам, разница была примерно в 8 раз


    1. newclaus
      29.05.2023 15:22

      Наверное в слове Dynamic


  1. kemsky
    29.05.2023 15:22

    Сеттеры и геттеры можно было собрать и скомпилировать так же как и делагат для методов.


  1. aamonster
    29.05.2023 15:22

    Погодите. Ваша задача что – в рантайме проверить, совпадают ли типы аргументов с требуемыми? Ну в смысле вы заранее не знаете ни тип аргументов делегата (узнаете при его установке), ни тип аргументов, с которыми его вызовут, и просто молитесь, чтобы они совпали?


    1. DemiEljar Автор
      29.05.2023 15:22

      Почему же "молюсь": тип аргумента подставляется на завершающем этапе работы библиотеки, а представленный механизм позволяет унифициоровать нижестоящий слой, сведя все обработчики к единому виду без необходимости ручного прописывания типов делегатов; иными словами - оперирование конкретными типами всплывает наверх

      Это что касается конкретно моего применения

      С другой стороны, в чистом виде, мне показалось это весьма неплохим способом абстрактизации и интересным кейсом


      1. aamonster
        29.05.2023 15:22

        Но всё же – если пользователь библиотеки вызвал делегат не с теми аргументами, узнаете вы об этом в рантайме?

        И единственная защита – что вызов не выполнится из-за несовпадения типов, т.е. ошибку мы "заметём под ковёр"? Если да, то лучше бы хотя бы сделать как в Delegate.DynamicInvoke – бросить exception.

        Сорри, если неправильно вас понял – со смартфона разбираться в коде не очень удобно.


        1. DemiEljar Автор
          29.05.2023 15:22

          Обработка ошибок кастинга в рантайме предусмотрена, может, она представлена не всеми возможными случаями, но на базовом уровне присутствует, 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);
              }
          }


          1. aamonster
            29.05.2023 15:22
            +1

            И архитектура решительно не даёт возможности перекинуть проверку типов в compile time? Сочувствую.
            Может, тогда подумать о кодогенерации – чтобы на обоих концах "трубы", через которую проходит вызов, была одинаковая сигнатура функции? Ну в смысле обвязка вокруг вашего "анонимного" делегата – только для контроля типов?


  1. ARad
    29.05.2023 15:22
    +1

    А использовать dynamic типы автор не пробовал? Там под капотом JIT компиляция... Должно быть быстро...


    1. DemiEljar Автор
      29.05.2023 15:22

      Увы, автор пробовал, но не запустилось, буду рад, если поделитесь примером кода или ссылкой на код)


      1. ARad
        29.05.2023 15:22
        +1

        У вас в репозитории нет ни одного примера использования ваших классов.
        Добавьте в репозитарий тесты производительности и будет более понятно какую задачу решаете и почему dynamic не запускается...


      1. ARad
        29.05.2023 15:22

        Вы так хотели? 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();
        }
        


        1. DemiEljar Автор
          29.05.2023 15:22

          Как я писал в нижестоящем ответе (https://habr.com/ru/articles/738346/comments/#comment_25597142), ваш вариант базируется на конкретном контексте, в случае использование "абстрактного" Delegate будет осложнен прямой вызов и, тем более, кастинг передаваемых аргументов


          1. ARad
            29.05.2023 15:22
            +1

            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;
            		}
            


  1. qw1
    29.05.2023 15:22
    +1

    Пытаюсь придумать применение этой штуке, и выходит что-то типа редактора GUI-форм, где разные user-control-ы могут выставлять разные свойства, и их нужно унифицированно редактировать/показывать. Но в таком случае чистый reflection более чем достаточен по скорости. А в случаях, где скорость критична, гонять все параметры через object — неправильно.


    1. nronnie
      29.05.2023 15:22
      +1

      Пытаюсь придумать применение этой штуке

      Например, кастомная реализация медиатора, или диспетчеризация сообщений из очереди.


  1. nronnie
    29.05.2023 15:22
    +1

    кастинг дженериков с заменой параметров невозможен.

    Возможен, но только в одну сторону. Это называется "контравариантность".

    Action<object> foo = x => Console.WriteLine(x);
    Action<string> bar = foo;
    bar("Hello, world!");
    

    Самый простой способ завернуть вызов метода в какой-нибудь Action<object> избежав при этом рефлексии это взять MethodInfo построить по нему Expression<Action<object>> и сделать ему Compile(). У меня где-то в исходниках есть кусок такого кода, если на досуге найду, то выложу здесь - там всего дюжины полторы строчек надо для этого.


    1. nronnie
      29.05.2023 15:22
      +4

      Вот идея в общих чертах:

      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);
      }
      


      1. mayorovp
        29.05.2023 15:22
        -1

        У вас тут лишний уровень индирекции вышел, если делать через DynamicMethod — можно избавиться от замыкания.


        1. nronnie
          29.05.2023 15:22

          Да, можно, конечно, но с System.Reflection.Emit работать, по-моему, куда сложнее чем с System.Linq.Expressions.


          1. mayorovp
            29.05.2023 15:22
            +1

            Не сильно-то и сложнее:


            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);
            }


            1. nronnie
              29.05.2023 15:22

              Да, неплохо, возьму на заметку.


            1. DemiEljar Автор
              29.05.2023 15:22

              Соглашусь, красивый вариант, но он также предполагает указание конкретного типа при конструировании результирующего делегата (см https://habr.com/ru/articles/738346/comments/#comment_25597142)


              1. mayorovp
                29.05.2023 15:22
                +2

                Доработать для произвольного 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);
                }


                1. DemiEljar Автор
                  29.05.2023 15:22

                  Весьма благодарен за развернутый ответ и за красивое решение)


      1. DemiEljar Автор
        29.05.2023 15:22

        А не проще тогда в случае вашего примера использовать максимально прострой код

        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);

        Если я не прав, то прошу меня поправить, в любом случае, ваш вариант мне очень понравился)


        1. nronnie
          29.05.2023 15:22

          А не проще тогда в случае вашего примера использовать максимально прострой код

          Да, но у меня тогда как раз и была такая ситуация как вы уже описали - сам Action<T> был доступен только в "нетипизированном" виде, поэтому явное приведение (T)value было невозможно. Я этот кусок кода выдернул сюда просто как пример как можно Expressions использовать для динамического создания делегатов.


          1. DemiEljar Автор
            29.05.2023 15:22

            Я вам благодарен за репрезентативный пример)


          1. qw1
            29.05.2023 15:22

            Я сразу подумал, почему так сложно, а не


            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);


            1. nronnie
              29.05.2023 15:22

              Да нет же, там откуда это взято код вообще другой - я его уже тут переделал, чтобы общая мысль была ясна. А в исходном там вообще были даже не делегаты Action<T>, а generic интерфейсы.


            1. mayorovp
              29.05.2023 15:22

              Возможно, просто там будет Expression.Lambda<Action<Delegate, object>> и первый аргумент тоже надо будет кастовать перед вызовом.


              1. nronnie
                29.05.2023 15:22

                Исходный код вообще вот так вот выглядел:

                    /// <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();
                        }
                    }
                

                Но, понятно, что если бы я его тут как есть разместил, то было бы вообще ничего непонятно :))


    1. DemiEljar Автор
      29.05.2023 15:22

      Да, я немного косо сформулировал свое утверждения, я имел в виду обратное преобразование