學習Source Generators之HelloWorld

饭勺oO發表於2024-03-28

介紹

源生成器是 C# 開發人員可以編寫的一種新元件,允許執行兩個主要操作:

  1. 檢索表示正在編譯的所有使用者程式碼的編譯物件。 可以檢查此物件,並且可以編寫適用於正在編譯的程式碼的語法和語義模型的程式碼,就像現在使用分析器一樣。
  2. 生成可在編譯過程中新增到編譯物件的 C# 原始檔。 也就是說,在編譯程式碼時,可以提供其他原始碼作為編譯的輸入。

結合使用這兩項操作能充分發揮源生成器的強大功能。 可以使用編譯器在編譯時構建的豐富後設資料檢查使用者程式碼。 然後,生成器將 C# 程式碼傳送回基於已分析資料的同一編譯。 如果你熟悉 Roslyn 分析器,可以將源生成器視為可發出 C# 原始碼的分析器。
源生成器作為編譯階段執行,如下所示:

源生成器是由編譯器與任何分析器一起載入的 .NET Standard 2.0 程式集。 它在可以載入和執行 .NET Standard 元件的環境中使用。
注意:目前只能用 .NET Standard 2.0 程式集作源生成器。

實現Hello Wolrd

接下來開始使用Source Genertor實現我們我HelloWorld程式。

建立專案

建立一個HelloWorld的控制檯專案。
將Program改成部分類。並新增一個Hello的部分方法。

namespace HelloWorld
{
    partial class Program
    {
        static void Main(string[] args)
        {
            Hello("Generated Code");
        }

        static partial void Hello(string name);
    }
}

接下來建立一個netstandard2.0的類庫。
命名成HelloWorld.Analysis。新增依賴Microsoft.CodeAnalysis.CSharp和Microsoft.CodeAnalysis.Analyzers。需要設定PrivateAssets=“all”。
完整配置如下:

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

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

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>

這裡需要注意的是Microsoft.CodeAnalysis.CSharp不宜使用太高版本,太高版本可能會出現無法正常生成程式碼的情況。
在HelloWorld專案中新增HelloWorld.Analysis的專案依賴。並設定OutputItemType="Analyzer" ReferenceOutputAssembly="false"
如下所示:

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

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<ItemGroup>
		<ProjectReference Include="..\HelloWorld.Analysis\HelloWorld.Analysis.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
	</ItemGroup>

</Project>

實現Generator

在HelloWorld.Analysis中新增HelloSourceGenerator類。繼承並實現ISourceGenerator介面。並且需要在類上加上Generator特性標籤。
然後再Exceute中實現我們的程式碼生成邏輯。

using Microsoft.CodeAnalysis;

namespace HelloWorld.Analysis
{
    [Generator]
    public class HelloSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static partial void Hello(string name) =>
            Console.WriteLine($""Hello: '{{name}}'"");
    }}
}}
";
            var typeName = mainMethod.ContainingType.Name;

            context.AddSource($"{typeName}.g.cs", source);
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}

在上面程式碼中,透過Compilation獲取Program程式入口的資訊。包括名稱空間,類名等等等。最後AddSource($"{typeName}.g.cs", source);表示我們把程式碼生成到.g.cs字尾的檔案中。

編譯

接下來啟動編譯專案,在HelloWorld的依賴項的分析器中會出現一個Program.g.cs檔案。
image.png
雙擊開啟可以看到生成的程式碼。並且會提示該檔案是自動生成的,無法編輯。
可以看到,檔案中我們實現了部分類Program中的部分方法Hello。
image.png

執行專案

啟動專案,可以看到我們成功輸出由Source Genertor生成的Hello方法的實現。
image.png

注意事項

細心的同學可能會看到我們編譯的時候會出現一個警告:
warning RS1036: “HelloWorld.Analysis.HelloSourceGenerator”: 包含分析器或源生成器的專案應指定屬性“true
建議我們在專案中新增EnforceExtendedAnalyzerRules的屬性。
當我們新增這個屬性後這個警告就會消失。

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

	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<LangVersion>latest</LangVersion>
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
	</ItemGroup>
</Project>

設定 EnforceExtendedAnalyzerRules 為 true 的作用就是提供 API 禁用分析功能,防止寫出分析器不支援的程式碼。設定 EnforceExtendedAnalyzerRules 為 true 時,有部分的 API 將會被提示不可用。具體API可以看: https://raw.githubusercontent.com/dotnet/roslyn-analyzers/2b6ab8d727ce73a78bcbf026ac75ea8a7c804daf/src/Microsoft.CodeAnalysis.Analyzers/Core/AnalyzerBannedSymbols.txt

Debug

前面我們直接編譯就生成了程式碼,打斷點也不會觸發。那麼我們如何除錯SourceGenerator呢?
可以使用Debugger.Launch();來觸發除錯。
在我們的執行程式碼中加入這一行。在編譯時會觸發除錯提示。

using Microsoft.CodeAnalysis;
using System.Diagnostics;

namespace HelloWorld.Analysis
{
    [Generator]
    public class HelloSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            Debugger.Launch(); //觸發Debug
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static partial void Hello(string name) =>
            Console.WriteLine($""Hello: '{{name}}'"");
    }}
}}
";
            var typeName = mainMethod.ContainingType.Name;

            context.AddSource($"{typeName}.g.cs", source);
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}

加入程式碼後,重新執行專案編譯操作。會彈出Debugger提示。
image.png
點選OK即可進入除錯模式。
image.png
如果不需要點選Cancel則可以跳過。

結語

本文初步的瞭解了SourceGenerator的功能以及使用和除錯的方式,後面的文章我們再來逐步深入學習。
文章程式碼倉庫地址https://github.com/fanslead/Learn-SourceGenerator

相關文章