JavaRush /Курсы /C# SELF /Source Generators и ...

Source Generators и System.Reflection.Emit

C# SELF
63 уровень , 4 лекция
Открыта

1. Введение

Давайте честно: большинство людей думают о генерации кода примерно так же, как о машинке для нарезки яиц — полезно, но редко жизненно необходимо. Однако современные .NET-проекты становятся всё сложнее, и автоматизация рутинной или шаблонной работы над кодом — это не просто "фишка", а важный инструмент повышения качества и производительности.

Source Generators — это механизм, появившийся начиная с C# 9 и .NET 5. Они, как супергерои на этапе компиляции, могут генерировать C#-код, который потом компилируется как часть вашего проекта. При этом они не вмешиваются в уже скомпилированный ассемблированный код во время выполнения, а лишь расширяют проект новыми исходниками до компиляции.

Ниже будут типовые сценарии использования Source Generators.

Автоматическое создание шаблонного кода

Во многих проектах приходится создавать однотипный код: конструкторы, методы ToString, сериализаторы, property change notification (INotifyPropertyChanged) и т.п. Source Generators могут генерировать этот код автоматически, избавляя разработчиков от рутины и риска опечаток.

Пример: Генератор ToString

Допустим, вы разрабатываете DTO-классы (Data Transfer Object) для передачи данных. Вам надо реализовать у каждого класса осмысленный ToString, в котором будут перечислены все свойства.

Вместо ручного копирования можно создать Source Generator, который для каждого класса с атрибутом [AutoToString] сгенерирует реализацию метода ToString. Так делают, например, библиотеки AutoToString.


// Ваш класс с атрибутом
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator сгенерирует (упрощённо):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Плюсы: код читаемый и всегда обновлён при добавлении новых свойств.

Упрощение сериализации и десериализации

Source Generators активно используются внутри стандартной библиотеки System.Text.Json для генерации быстрого кода сериализации и десериализации. До появления генераторов сериализация часто требовала рефлексии (дорого по производительности), а теперь — сгенерированный код делает всё быстро и безопасно.

Что получает разработчик? Указываете [JsonSerializable(typeof(MyType))] — и генератор создаёт производительный код сериализации для этого типа.

Генерация кода для конфигураций, мапперов, DI-контейнеров

  • Конфиги: генераторы автоматически создают классы конфигурации на основе JSON-файлов.
  • Мапперы: например, проект Mapster через генераторы создаёт маппинг между типами без ручного копирования полей.
  • Dependency Injection: некоторые контейнеры (например, StrongInject) используют генераторы для кода регистрации сервисов.

Интеграция со сторонней инфраструктурой

Некоторые генераторы анализируют внешние ресурсы (описание API, GraphQL-схемы, Thrift-протоколы и др.) и генерируют C#-классы для работы с ними. Это избавляет от ручного обновления кода при изменении контрактов.

Проверка и диагностика кода на этапе компиляции

Source Generators можно использовать и для проверки кода: "Если в проекте встречается метод X, но не выполнено условие Y — предупреждение в компиляторе!". Так работают многие линтеры и analyzers, но генератор может добавить свои сообщения или даже подсунуть готовый код-заглушку.

2. Свой Source Generator для автоматической сериализации

Рассмотрим маленький пример "в лоб": генератор, который для класса с атрибутом [AutoJson] генерирует метод для сериализации в JSON-формат.

Код ниже — лишь иллюстрация; в реальной жизни используйте System.Text.Json.SourceGeneration.


// Вручную пишем:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Генератор добавит:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Схема взаимодействия Source Generator и проекта

flowchart TD
    A(Ваш Source Code) -->|Компилятор вызывает| B(Source Generator)
    B -->|Добавленный Code| C(Новые .cs-файлы)
    C --> D(Компиляция проекта)
  • Сначала компилятор (Roslyn) анализирует ваши исходники.
  • Потом вызывает "ваш" генератор (реализующий ISourceGenerator).
  • Генератор добавляет новые .cs-файлы, которые становятся частью общего дерева компиляции.
  • В итоге сборка содержит и ваш, и сгенерированный код.

Специализированные задачи: что ещё можно генерировать

  • Биндинги для нативных библиотек (обёртки под C-код или WinAPI).
  • AOP: автологирование вызовов, аспекты (в духе Fody, PostSharp и др.).
  • Описание API для автогенераторов документации.
  • Обработка внешних ресурсов: SVG, SQL, Razor — генератор создаёт strongly-typed классы для типобезопасного доступа.

3. Динамическая генерация кода с помощью System.Reflection.Emit

Если Source Generators генерируют C#-код до сборки, то System.Reflection.Emit — инструмент для магии во время исполнения. Ваша программа может сама создавать новые типы, методы и даже сборки — прямо на лету!

Звучит пугающе? Немного. Но иногда без этого не обойтись: например, если вы пишете фреймворки для динамического проксирования (AOP, профилирование, мокинг), генераторы сериализаторов под runtime-данные, динамические ORM и т.д.

Когда нужен Reflection.Emit

  • Типы заранее не известны (пользователь задаёт структуру на лету).
  • Динамическое проксирование (обёртки для перехвата вызовов).
  • Высокопроизводительная сериализация (например, protobuf-net).
  • Плагины и скриптовые движки со сложными сценариями загрузки.

Что можно создавать через Reflection.Emit

  • AssemblyBuilder — создание новой сборки.
  • ModuleBuilder — модуль в сборке.
  • TypeBuilder — описание нового типа.
  • MethodBuilder — метод с IL-кодом.
  • PropertyBuilder, FieldBuilder, EventBuilder — свойства, поля, события.

Мини-пример: сборка нового класса на лету

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Создаём сборку и модуль
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Создаём новый класс
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Добавляем публичное строковое свойство Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Готово! Создаём Type
        return typeBuilder.CreateTypeInfo();
    }
}

Теперь можно использовать этот тип как обычный C#-объект, например, через рефлексию:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# в лицах");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# в лицах

Так рождаются ORM, прокси, сериализаторы, профилировщики и даже некоторые тестовые фреймворки.

4. Полезные нюансы

Source Generators vs Reflection.Emit: кто кого?

Source Generators работают на этапе компиляции: делают ваш исходный код умнее, а результат попадает в скомпилированную сборку. Их нельзя использовать для генерации кода на основании данных, полученных уже во время работы программы.

Reflection.Emit работает в рантайме: позволяет создавать сборки, типы и методы динамически, но такой код сложнее отлаживать и поддерживать.

Когда что использовать?

Аналогия из жизни:

  • Source Generators — как фабрика, выпускающая готовые детали до сборки машины.
  • Reflection.Emit — как инженер, который на лету приваривает к машине ракетный двигатель прямо во время поездки.

Особенности и подводные камни

  • Генерируемый код может быть сложен для отладки. У генераторов часто есть возможность сохранить исходники на диск — ищите их в папке obj\Generated.
  • Reflection.Emit создаёт сборки в памяти, которые не выгружаются из AppDomain. Используйте AssemblyBuilderAccess.RunAndCollect для временных сборок (если поддерживается).
  • Не злоупотребляйте Reflection.Emit для простых задач — иногда генератор исходников или обычный шаблон кода проще и надёжнее.
  • Генераторы требуют понимания Roslyn (для Source Generators) и IL (для Reflection.Emit).

Source Generators и Reflection.Emit

Критерий Source Generators Reflection.Emit
Используется На этапе компиляции В рантайме
Результат C#-исходники, часть сборки IL-код, новые типы/сборки
Обычные сценарии Автогенерация шаблонного кода, DI, маппинг, сериализация Прокси, динамические ORM, спец. runtime-пайплайны
Сложность использования Средняя, нужны знания Roslyn Высокая, нужны знания IL
Поддержка IDE и отладки Отличная (видно исходники) Сложная
Производительность Очень высокая Может быть высокой

5. Практические сценарии

1. Генерация strongly-typed API

Организация предоставляет OpenAPI-спецификацию. Генератор анализирует её и создаёт классы контроллеров, DTO и клиентский код для работы с REST API — типобезопасно и с поддержкой IntelliSense.

Код (псевдо):
// Spec: GET /users -> returns User[]
// Source Generator сгенерирует:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Автоматический injection/DI-контейнер (Compile-time IoC)

Генераторы создают билдеры для регистрации зависимостей и построения графа объектов. Не нужно вручную писать services.AddSingleton<IMyService, MyService>().

Код (псевдо):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator сгенерирует:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Динамические прокси — пример на Reflection.Emit

Библиотека Castle DynamicProxy строит прокси-типы, перехватывающие вызовы методов — основа для AOP, логирования, трейсинга, мокинга.

Код (упрощён):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // Вызов перехвачен, можно логировать/менять результат

4. Быстрая сериализация без рефлексии

Вместо того чтобы в рантайме строить описания типов рефлексией (медленно), Source Generators могут сгенерировать код сериализации/десериализации заранее — максимально быстро и без накладных расходов.

2
Задача
C# SELF, 63 уровень, 4 лекция
Недоступна
Динамическое создание класса с использованием Reflection.Emit
Динамическое создание класса с использованием Reflection.Emit
1
Опрос
Рефлексия, 63 уровень, 4 лекция
Недоступен
Рефлексия
Рефлексия и динамические типы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ