Метапрограммирование в 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