[Перевод] C#: Знакомство с генераторами исходного кода

image

Мы рады представить вам превью генераторов исходного кода. Это новая возможность, которая позволяет разработчикам C# анализировать пользовательский код и создавать новые файлы C#, которые в свою очередь могут добавляться в процесс компиляции. Это происходит при помощи нового компонента — генератора исходного кода (Source Generator).

Чтобы начать работу с генераторами понадобятся последние .NET 5 preview и Visual Studio preview. Примечание: чтобы построить генератор исходного кода пока требуется Visual Studio. Это будет изменено в следующем превью .NET 5.


Что такое генератор исходного кода

Генератор исходного кода — это фрагмент кода, который выполняется во время компиляции, проверяет программу и создает дополнительные файлы, которые затем компилируются вместе с остальной частью кода.

Вот две основные возможности, которые он позволяет реализовать:


  • Получать объект компиляции, который представляет весь компилируемый пользовательский код. Данный объект может быть проанализирован, и вы можете написать код, который работает с синтаксической и семантической моделями компилируемого кода, как в случае с современными анализаторами.
  • Генерировать исходные C# файлы, которые могут быть добавлены в объект компиляции в процессе компиляции. Иными словами, вы можете добавить исходный код в качестве входных данных для компиляции в процессе компиляции.

Эти две особенности делают генераторы довольно полезными. Вы можете проанализировать пользовательский код со всеми метаданными, которые создаются во время компиляции, а затем отправить код C# обратно в компиляцию. Если вы знакомы с анализаторами Roslyn, под генератором исходного кода можно понимать анализатор, который может производить исходный код C#.

Генераторы выполняются как фаза компиляции:
image

Генератор исходного кода представляет собой сборку .NET Standard 2.0, которая загружается компилятором вместе с анализаторами и может использоваться в средах, где доступны компоненты .NET Standard.

Давайте рассмотрим отдельные случаи, в которых они могут понадобиться.


Примеры сценариев, где может быть выгодно использование генераторов исходников.

Важнейший аспект генераторов — это не то, что они собой представляют, а то, что позволяют реализовать.

Сегодня существует три основных подхода к проверке пользовательского кода и генерированию кода на основе ее результатов: рефлексия времени выполнения, внедрение IL (IL weaving) и жонглирование задачами MSBuild. Генераторы исходников могут улучшить каждый из них.

Рефлексия — это мощный инструмент в .NET. Существует бесконечное множество сценариев его использования. Часто рефлексию используют для анализа пользовательского кода при старте программы и использования его результатов в последующем.

К примеру, ASP.NET Core во время первого запуска веб-сервиса использует рефлексию, чтобы выявить определяемые конструкции и подключить контроллеры, страницы razor и другие компоненты. Хоть это и позволяет вам писать простой код с мощными абстракциями, это приводит к снижению производительности во время выполнения: когда веб-служба или приложение впервые запускается, они не могут принимать запросы до тех пор, пока весь код рефлексии, который анализирует код, не будет выполнен. Хоть эти затраты на производительность и невелики, вы не можете их оптимизировать.

В случае использования генератора фаза определения контроллеров при старте будет происходить во время компиляции — после анализа исходного кода и будет создан новый код для подключения приложения. Это может ускорить время запуска за счет переноса операции времени выполнения в фазу компиляции.

Генераторы исходников могут повысить производительность способами, которые не ограничиваются рефлексией времени выполнения для обнаружения типов. Некоторые сценарии включают в себя неоднократный вызов задачи MSBuild C# (называемой CSC) для проверки данных компиляции. Как вы понимаете, вызов компилятора более одного раза влияет на время построения вашего приложения. Мы анализируем как можно использовать генераторы во избежание необходимости жонглирования задачами MSBuild, поскольку генераторы исходного кода не только предлагают некоторые преимущества в производительности, но и позволяют инструментам работать на правильном уровне абстракции.

Еще одна возможность, которую могут предложить генераторы исходников — это избежать использования некоторых "строчно-ориентированных" (“stringly-typed”) API. Такие, к примеру, используются в ASP.NET Core для маршрутизации между контроллерами и страницами razor. С генератором исходного кода маршрутизация может быть строго типизирована и необходимые строки будут сгенерированы во время компиляции. Это поможет избежать случаев, когда ошибочный строковый литерал приводит к тому, что запрос не попадает в правильный контроллер.

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


Генераторы исходников и Ahead-of-Time (AOT) компиляция

Генераторы исходного кода могут помочь устранить основные барьеры при оптимизации linker-based и AOT-компиляций. Многие фреймворки и библиотеки активно используют рефлексию, например System.Text.Json, System.Text.RegularExpressions; фреймворки ASP.NET Core и WPF, обнаруживают и / или выделяют типы из пользовательского кода во время выполнения.

Также многие популярные пакеты NuGet широко используют рефлексию для определения типов во время выполнения. Эти пакеты имеют важное значение для многих приложений .NET, поэтому "связность" ( “linkability”) и способность вашего кода использовать оптимизацию AOT компилятора играют большую роль. Мы планируем работу с сообществом OSS для поиска новых возможностей использования генераторов исходного кода в пакетах, и проанализировать как они могут улучшить экосистему .NET.


Hello World-версия генератора

Чтобы показать ключевые моменты написания собственного генератора исходного кода, рассмотрим следующий пример.

Допустим, нам нужно позволить пользователям всегда иметь доступ к сообщению “Hello World” и всем синтаксическим деревьям, доступным во время компиляции. Они могли бы вызывать его следующим образом:

public class SomeClassInMyCode
{
    public void SomeMethodIHave()
    {
        HelloWorldGenerated.HelloWorld.SayHello(); 
        // вызывает Console.WriteLine("Hello World!") и затем выводит синтаксические деревья
    }
}

Со временем мы сделаем шаблоны чтобы упростить этот этап, а пока сделаем вручную:

1.Создадим проект стандартной библиотеки .NET.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

  <PropertyGroup>
    <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.20207.2" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0-beta2.final" PrivateAssets="all" />
  </ItemGroup>

</Project>

2.Изменим или создадим файл C#, в котором определим наш генератор.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;

namespace MyGenerator
{
    [Generator]
    public class MySourceGenerator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {
            // TODO - здесь размещается генератор исходного кода
        }

        public void Initialize(InitializationContext context)
        {
             // инициализация здесь не требуется 
        }
    }
}

Здесь нужно применить атрибут Microsoft.CodeAnalysis.Generator и реализовать интерфейс Microsoft.CodeAnalysis.ISourceGenerator.

3.Добавим сгенерированный исходный код в компиляцию.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace SourceGeneratorSamples
{
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {

            // код, который мы будем внедрять в компиляцию пользователя
            var sourceBuilder = new StringBuilder(@"

using System;
namespace HelloWorldGenerated
{
    public static class HelloWorld
    {
        public static void SayHello() 
        {
            Console.WriteLine(""Привет из сгенерированного кода!"");
            Console.WriteLine(""В компиляции данной программы есть следующие синтаксические деревья"");
");

           // используя контекст, получим список синтаксических деревьев в пользовательской компиляции
            var syntaxTrees = context.Compilation.SyntaxTrees;
            // добавим путь к файлу каждого дерева в класс, который мы создаем
            foreach (SyntaxTree tree in syntaxTrees)
            {
                sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");");
            }
            //завершаем создание внедряемого кода
            sourceBuilder.Append(@"
        }
    }
}");
            // внедрим созданный исходник в компиляцию пользователя
            context.AddSource("helloWorldGenerator", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
        }

        public void Initialize(InitializationContext context)
        {
              // инициализация здесь не требуется 
        }
    }
}

4.Добавим генератор из проекта в качестве анализатора и превью к LangVersion в файл проекта.

<!-- Это относится к группе свойств верхнего уровня -->
<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

<!-- Добавим новую ItemGroup, заменив пути и имена соответствующим образом -->
<ItemGroup>
<!--  Обратите внимание, что это не "обычный" ProjectReference. 
Понадобятся дополнительные атрибуты 'OutputItemType' и 'ReferenceOutputAssmbly'. -->
    <ProjectReference Include="path-to-sourcegenerator-project.csproj" 
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
</ItemGroup>

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

При написании кода в Visual Studio вы увидите, что генератор запущен и сгенерированный код доступен для вашего проекта. Теперь можно получить к нему доступ, как будто он был создан нами:

public class SomeClassInMyCode
{
    public void SomeMethodIHave()
    {
        HelloWorldGenerated.HelloWorld.SayHello(); 
        // вызывает Console.WriteLine("Hello World!") и затем выводит синтаксические деревья
    }
}

Примечание: на данный момент необходимо перезапускать Visual Studio, чтобы корректно заработал IntelliSense и пропали ошибки

Вот что еще можно сделать при помощи генераторов:


  • автоматически реализовывать интерфейсы для классов с атрибутами, например, INotifyPropertyChanged,
  • генерировать файлы настроек на основании данных, полученных из SourceGeneratorContext,
  • сериализовать значения из классов в JSON-строки и т. д.

Некоторые реализации приведены в Source Generators Cookbook.
Также можно посмотреть примеры на GitHub.

Как уже упоминалось ранее, мы работаем над улучшением опыта работы с генераторами — над добавлением шаблонов, обеспечением бесшовной работы IntelliSense и навигации, отладкой и повышением быстродействия и производительности в Visual Studio.


Сейчас генераторы доступны в превью

Это первое превью генераторов исходного кода. Его цель — позволить авторам библиотек познакомиться с данной возможностью и получить обратную связь. От одного превью к другому возможны изменения в API и характеристиках генераторов. Мы намерены сделать их общедоступными с C# 9, а позже в этом году мы планируем стабилизировать API и функции, которые он предоставляет.

Призыв к разработчикам C# библиотек: пробуйте! Если у вас есть .NET библиотека, написанная на C# – сейчас отличное время попробовать генераторы и посмотреть подходят ли они вам. Если ваша библиотека активно использует рефлексию, есть шанс в какой-то степени выиграть.

В этом вам помогут следующие материалы:

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

На данном этапе Visual Studio позволяет выполнять базовое редактирование, однако текущее решение не является “версией 1.0”. В течение долгого времени мы будем исследовать различные варианты исполнения, прежде чем определимся с конкретным из них. Особое внимание перед выпуском .NET 5 будет заключаться в улучшении опыта редактирования генераторов исходного кода. Кроме того, планируется внести изменения в API, чтобы учесть обратную связь от команд-партнеров и нашего сообщества OSS.

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

Ниже приведен список вопросов, которые могут возникнуть. Мы будем обновлять его новыми вопросами по мере поступления.


Чем генераторы исходного кода отличаются от других способов метапрограммирования — макросов, плагинов компилятора?

Генераторы исходного кода — это одна из форм метапрограммирования, поэтому вполне естественно сравнивать их с аналогичными возможностями других языков, например, макросами. Ключевое отличие заключается в том, что генераторы исходного кода не позволяют вам изменять пользовательский код. Мы считаем это ограничение значительным преимуществом, поскольку оно позволяет делать пользовательский код предсказуемым относительно того, что он фактически делает во время выполнения. Мы признаем, что переписывание пользовательского кода-это очень мощная функция, но мы вряд ли внедрим эту возможность.


В чем отличия генераторов исходного кода от поставщиков типов F#?

Частично генераторы были вдохновлены поставщиками типов F#. Но у них есть несколько отличительных черт. Основная заключается в том, что поставщики типов являются частью языка F# и предоставляют собственные и определяемые типы, свойства и методы в памяти на основе внешнего источника. Генераторы исходного кода — это функция компилятора, которая анализирует код C#, опционально с другими файлами, и генерирует исходный код C# для включения его обратно в компиляцию.


Стоит ли теперь удалять всю рефлексию?

Нет. Рефлексия — это отличный инструмент. У нее есть нюансы в производительности и “связности”, которые в некоторых случаях могут быть исправлены при помощи генераторов. Мы рекомендуем тщательно оценить подойдут ли они в вашем случае.


Чем отличаются генераторы от анализаторов?

Генераторы исходного кода похожи на анализаторы, поскольку оба являются функциями компилятора и позволяют подключаться к компиляции. Их ключевое отличие в том, что анализаторы в конечном счете выдают результаты диагностики, которые могут быть использованы для правки кода. Генераторы исходников в конечном итоге выдают исходный код C#, который добавляется в компиляцию. Некоторые различия обсуждаются в дизайн-документации.


Могу ли я изменить/переписать существующий код при помощи генератора?

Нет. Как уже упоминалось ранее, генераторы исходного кода не позволяют изменять/переписывать код пользователя. Они могут только дополнить компиляцию, добавив в нее исходные файлы C#.


Когда генераторы выйдут из превью?

Мы планируем поставлять генераторы вместе с C# 9. Однако если они не будут готовы вовремя мы придержим их в превью, гарантируя возможность их использования.


Могу ли я изменить TFM в генераторе?

Технически да. Генераторы являются компонентами .NET Standard 2.0 и вы можете менять TFM как и для любого проекта. Сейчас они загружаются в проект только как компоненты .NET Standard 2.0.


Планируется ли внедрение генераторов в Visual Basic или F#?

Пока генераторы доступны только в С#. Так как это первое превью, они претерпят еще много изменений. На данный момент реализовывать их в VB не планируется. Если вы разрабатываете на F# и хотите видеть их в этом языке — вы можете внести соответствующие предложения.


Появятся ли новые проблемы совместимости для библиотек?

Это зависит от того, как создаются библиотеки. Поскольку VB и F# в настоящее время не поддерживают генераторы исходного кода, нужно избегать разработки своих функций таким образом, чтобы им требовался генератор. В идеале они должны иметь резервные возможности для рефлексии. Авторам библиотек следует это иметь в виду. Мы ожидаем, что генераторы будут использоваться для расширения, а не замены текущего опыта разработчиков.


Почему не работает Intellisense для сгенерированного кода? Почему Visual Studio указывает на ошибку, даже если билд успешен?

Чтобы убрать эти ошибки после построения генератора нужно перезапустить Visual Studio. Текущая интеграция с ней пока сыра. В дальнейшем эта проблема будет исправлена и необходимости в перезагрузке Visual Studio не будет.


Можно ли делать дебаггинг или переходить к сгенерированному коду в Visual Studio?

В конечном итоге навигация и отладка сгенерированного исходного кода будет поддерживаться в Visual Studio. На этапе превью данная возможность не реализована


Как можно поставлять свой генератор?

Можно поставлять их как пакеты NuGet, таким же образом, как и анализаторы. С опытом поставки анализатора можно легко поставлять и генераторы.


Будет ли Microsoft заниматься разработкой генераторов?

В конечном итоге да. Но чтобы предусмотреть различные сценарии многое придется изменить. В настоящее время нет примерных сроков, когда генераторы от Microsoft увидят свет.


Зачем нужно использовать превью LangVersion для работы с генераторами?

Хотя генераторы исходного кода технически не являются функцией языка C#, они находятся в превью. Вместо того чтобы вводить новую настройку только для них, мы решили, что будет проще использовать существующий переключатель, который позволяет просматривать языковые возможности компилятора C#.

© Habrahabr.ru