Реализуем рефлексию при помощи source generators

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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


Изначальной идеей было создать библиотеку которая бы реализовывала часть стандартной рефлексии, работала бы быстрей чем стандартная и при этом прекрасно себя чувствовала бы во время AOT компиляции. И source generator-ы нам в этом очень помогут.


Как использовать


Думаю стоит начать с того как пользоваться этой новой рефлексией. Чтобы подключить её, достаточно установить NuGet пакет Apparatus.AOT.Reflection:


dotnet add package Apparatus.AOT.Reflection

Дальше мы сможем воспользоваться им следующим образом:


public class User
{
    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }
}

public static void Main()
{
    var user = new User();
    var properties = user.GetProperties().Values;
    foreach (var property in properties)
    {
        Console.WriteLine(property.Name);
    }
}

В результате мы увидим имена пропертей.


FirstName
LastName

Также это работает и для enum-ов:



public enum UserKind
{
    User,
    Admin
}

// ...

public static void Main()
{
    var values = EnumHelper.GetEnumInfo<UserKind>();
    foreach (var value in values)
    {
        Console.WriteLine(value.Name);
    }
}

Результат:


User
Admin

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


Вот как это выглядит:


var requiredProperties = _user
    .GetProperties()
    .Values
    .Where(o => o.Attributes.Any(attr => attr is RequiredAttribute))
    .ToArray();

foreach (var requiredProperty in requiredProperties)
{
    if (requiredProperty.TryGetValue(_user, out var value))
    {
        Console.WriteLine($"{requiredProperty.Name} => {value}");
    }
}

Это применимо и к enum-ам:


public enum AccountKind
{
    [Description("User account")]
    User,
    [Description("Admin account")]
    Admin,
    [Description("Customer account")]
    Customer,
    [Description("Manager account")]
    Manager
}

// ...

var values = EnumHelper.GetEnumInfo<AccountKind>();
foreach (var value in values)
{
    var description = value.Attributes
        .OfType<DescriptionAttribute>()
        .First();

    Console.WriteLine($"{value.Name} => {description.Description}");
}

Производительность


Предлагаю рассмотреть производительность на следующем примере. Давайте представим что нам нужно найти проперти с атрибутом Required и под именем FirstName.
Если таковая существует, то достаем значение этой проперти. В противном случае возвращаем пустую строку. Реализация будет немного странноватой. Это потому что не хотелось бы измерять скорость выполнения LINQ запроса, но основная идея должна быть предельно ясна.


Вот пример того как это выглядит если использовать стандартную рефлексию:


var type = _user.GetType();
var property = type.GetProperty(nameof(User.FirstName));

var required = false;
foreach (var o in property.GetCustomAttributes())
{
    if (o.GetType() == typeof(RequiredAttribute))
    {
        required = true;
        break;
    }
}

if (required)
{
    return (string)property.GetMethod?.Invoke(_user, null);
}

return string.Empty;

А вот пример с aot рефлексией:


var entries = _user.GetProperties();
var firstName = entries[nameof(User.FirstName)];

var required = false;
foreach (var o in firstName.Attributes)
{
    if (o is RequiredAttribute)
    {
        required = true;
        break;
    }
}

if (required)
{
    if (firstName.TryGetValue(_user, out var value))
    {
        return (string)value;
    }

    return string.Empty;
}

return string.Empty;

И результаты бенчмарка:


BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1165 (21H1/May2021Update)
11th Gen Intel Core i7-11700KF 3.60GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.100-preview.7.21379.14
  [Host]     : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
  DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT

|        Method |        Mean |    Error |   StdDev |  Gen 0 | Allocated |
|-------------- |------------:|---------:|---------:|-------:|----------:|
|    Reflection | 1,758.91 ns | 2.714 ns | 2.406 ns | 0.1278 |   1,072 B |
| AOTReflection |    16.01 ns | 0.090 ns | 0.075 ns |      - |         - |

Как мы видим AOT.Reflection на много быстрей в сравнении с обычной рефлексией.


Теперь посмотрим на производительность для enum-ов в ситуации когда нам нужно достать значения атрибута DescriptionAttribute из значения перечисления.
Это будет иметь следующий вид:


var attributes = _account.GetEnumValueInfo().Attributes;
for (int i = 0; i < attributes.Length; i++)
{
    var attribute = attributes[i];
    if (attribute is DescriptionAttribute descriptionAttribute)
    {
        return descriptionAttribute.Description;
    }
}

return "";

Результаты:


|              Method |       Mean |     Error |    StdDev |  Gen 0 | Allocated |
|-------------------- |-----------:|----------:|----------:|-------:|----------:|
|        GetValuesAOT |   6.253 ns | 0.0394 ns | 0.0329 ns |      - |         - |
| GetValuesReflection | 734.563 ns | 2.3173 ns | 1.9351 ns | 0.0324 |     272 B |

И опять AOT рефлексия работает на много быстрей.


Полный код бенчмарков можно найти тут.


Ограничения


Я бы рекомендовал быть очень осторожным во время использования AOT рефлексии внутри generic методов, поскольку, на данный момент, нету эффективного способа чтобы их проанализировать и понять необходимые сигнатуры. Это значить что никакой кодогенерации не случится вовсе. В результате мы получим ошибку во время выполнения.
Рассмотрим следующий пример:


public class Program
{
    public static string? GetDescription<T>(T enumValue)
        where T : Enum
    {
        return enumValue
            .GetEnumValueInfo()
            .Attributes
            .OfType<DescriptionAttribute>()
            .FirstOrDefault()
            ?.Description;
    }

    public static void Main()
    {
        var account = AccountKind.Admin;
        Console.WriteLine(GetDescription(account));
    }
}

Если мы его запустим, то получим exception, поскольку source generator не смог понять и используемые сигнатуры. Тип T для него загадка.
Но мы можем это починить небольшим трюком:


public class Program
{
    private void DontCallMe()
    {
        EnumHelper.GetEnumInfo<AccountKind>();
    }

    public static string? GetDescription<T>(T enumValue)
        where T : Enum
    {
        return enumValue
            .GetEnumValueInfo()
            .Attributes
            .OfType<DescriptionAttribute>()
            .FirstOrDefault()
            ?.Description;
    }

    public static void Main()
    {
        var account = AccountKind.Admin;
        Console.WriteLine(GetDescription(account));
    }
}

Обратите внимание на метод DontCallMe. Мы не собираемся его использовать вовсе. Он здесь, чтобы помочь source generator-у понять что от него хотят. Теперь, если запустить пример, все отработает как нужно.
Также проблема существует и с рефлексией пропертей, и мы можем использовать такой же трюк чтобы её избежать.


Что работает


На данный момент работают только публичные проперти и enum-ы. Если рассматривать поддержку на приватных членов, то тут не так все просто. Их добавление будет означать просадку в производительности. Я собираюсь посмотреть на это чуть позже.


Конец


Ссылки: Github, Nuget

Источник: https://habr.com/ru/post/578190/


Интересные статьи

Интересные статьи

Публикуем исходный код программных пакетов для роботов победивших в конкурсе open-source пакетов на ROS. Вы можете использовать их в своих роботах или продолжить их разработку вместе с авторами. Побед...
Будущее здесь — безо всяких преувеличений. В нашей публикации Третий глаз для незрячих мы рассказывали о том, как можно облегчить жизнь незрячим людям при помощи нескольк...
Hal 9000 прекрасно читал по губам, правда, по-английски Нейросети сейчас умеют многое, и постепенно их обучают все большему количеству умений. На днях стало известно о том, что объединенная ...
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...
Уже на этой неделе в Санкт-Петербурге пройдет IT-фестиваль TechTrain. Одним из спикеров будет Ричард Столлман. Embox тоже участвует в фестивале, и конечно мы не могли обойти вниманием тему СПО. П...