Метапрограммирование в C#: от рефлексии до кодогенерации

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Метапрограммирование в C#: Когда код пишет код

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

Зачем это нужно? Представьте, что вы пишете десятки однотипных классов для работы с JSON, создаёте сложные системы плагинов или ленитесь вручную вызывать сотни методов через отражение. Метапрограммирование в C# позволяет автоматизировать рутину, создавать гибкие фреймворки и ускорять разработку в разы. Сегодня мы разберём три кита этой техники: рефлексию, деревья выражений и динамическую генерацию кода.

Эта статья — для тех, кто уже уверенно пишет на C#, но хочет заглянуть под капот CLR и научиться магии, которая отличает сеньора от мидла. Поехали!

Рефлексия (Reflection): Рентген для вашего кода

Рефлексия — это фундамент метапрограммирования в C#. Она позволяет получить информацию о типах, методах, свойствах и полях во время выполнения, а также динамически вызывать их. Представьте, что вы смотрите на объект, как на открытую книгу, и можете прочитать любую её страницу.

Получение информации о типе

Самый простой способ — использовать ключевое слово typeof или метод GetType():

Type stringType = typeof(string);
Console.WriteLine($"Имя: {stringType.Name}"); // String
Console.WriteLine($"Пространство имен: {stringType.Namespace}"); // System
Console.WriteLine($"Это класс? {stringType.IsClass}"); // True

Динамический вызов методов

Допустим, у вас есть класс Calculator, и вы хотите вызвать его метод Add, имя которого известно только в рантайме (например, из конфигурации):

public class Calculator
{
    public int Add(int a, int b) => a + b;
}

// Используем рефлексию
Calculator calc = new Calculator();
Type calcType = typeof(Calculator);
MethodInfo? addMethod = calcType.GetMethod("Add");

if (addMethod != null)
{
    int result = (int)addMethod.Invoke(calc, new object[] { 5, 3 });
    Console.WriteLine($"Результат: {result}"); // 8
}

Когда рефлексия полезна?

  • Сериализация / десериализация — библиотеки вроде Newtonsoft.Json активно используют рефлексию.
  • Плагины — загрузка внешних сборок и вызов их методов.
  • Тестирование — доступ к приватным полям и методам (хотя лучше так не делать).

Важно! Рефлексия — медленная операция. Если вы вызываете один и тот же метод тысячи раз, задумайтесь о кэшировании MethodInfo или использовании делегатов.

Деревья выражений (Expression Trees): Код как данные

Деревья выражений — это способ представить код в виде структуры данных (дерева). Вы можете анализировать, модифицировать и даже компилировать этот код «на лету». Это мощный инструмент для создания ORM (Entity Framework), валидаторов и динамических запросов.

Создание простого дерева

Вместо того чтобы писать лямбду x => x * 2, мы строим её вручную:

using System.Linq.Expressions;

// Создаём параметр x
ParameterExpression param = Expression.Parameter(typeof(int), "x");
// Создаём константу 2
ConstantExpression constant = Expression.Constant(2);
// Создаём умножение x * 2
BinaryExpression body = Expression.Multiply(param, constant);
// Собираем всё в лямбду: x => x * 2
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);

// Компилируем и выполняем
Func<int, int> compiled = lambda.Compile();
Console.WriteLine(compiled(5)); // 10

Продвинутый пример: Динамический маппер

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

public static class Mapper
{
    private static readonly Dictionary<string, object> _cache = new();

    public static TTarget Map<TSource, TTarget>(TSource source) where TTarget : new()
    {
        string key = $"{typeof(TSource).FullName}->{typeof(TTarget).FullName}";
        if (!_cache.ContainsKey(key))
        {
            var sourceParam = Expression.Parameter(typeof(TSource), "source");
            var newTarget = Expression.New(typeof(TTarget));
            var memberBindings = new List<MemberBinding>();

            foreach (var targetProp in typeof(TTarget).GetProperties())
            {
                var sourceProp = typeof(TSource).GetProperty(targetProp.Name);
                if (sourceProp != null && targetProp.PropertyType == sourceProp.PropertyType)
                {
                    var sourceValue = Expression.Property(sourceParam, sourceProp);
                    memberBindings.Add(Expression.Bind(targetProp, sourceValue));
                }
            }

            var memberInit = Expression.MemberInit(newTarget, memberBindings);
            var lambda = Expression.Lambda<Func<TSource, TTarget>>(memberInit, sourceParam);
            _cache[key] = lambda.Compile();
        }

        return ((Func<TSource, TTarget>)_cache[key])(source);
    }
}

// Использование
var person = new Person { Name = "Алексей", Age = 30 };
var dto = Mapper.Map<Person, PersonDto>(person);
Console.WriteLine($"{dto.Name}, {dto.Age}"); // Алексей, 30

Этот маппер компилирует код один раз для каждой пары типов, а затем использует готовый делегат. Он работает в десятки раз быстрее, чем чистая рефлексия!

Динамическая генерация кода: Roslyn и IL

Самый высокий уровень метапрограммирования — генерация кода на лету. Вы можете создавать целые классы, компилировать их в память и использовать. Для этого в C# есть два основных подхода: генерация IL-кода (через System.Reflection.Emit) и использование компилятора Roslyn.

Генерация кода с помощью Roslyn

Roslyn — это компилятор C# как сервис. Вы можете подать ему строку с кодом и получить сборку. Это идеально для скриптов, плагинов или пользовательских формул.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

public static class CodeGenerator
{
    public static Assembly? CompileCode(string code)
    {
        var tree = SyntaxFactory.ParseSyntaxTree(code);
        var compilation = CSharpCompilation.Create("DynamicAssembly")
            .WithOptions(new CSharpComp                

Блоги

Рекомендаций по книгам