Как стать автором
Обновить

Обезличенный вызов делегатов в C#

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров5.9K

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

Как говорится: сидел, никого не трогал, программировал на 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): при необходимости можно масштабировать до требуемой размерности.

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии34

Публикации

Истории

Работа

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн