在阅读本文前需掌握源代码生成器相关知识C#源代码生成器深入讲解一
C#源代码生成器深入讲解二—增量生成器
源代码生成器有个非常大的弊病,每次都会遍历所有的语法树来分析,这样就有个问题,每次可能只修改了很少一部分或者只有很少一部分的代码需要分析,而增量源代码生成器可以理解为在之前的工作上做了一个筛选的动作,通过自定义的条件来过滤语法树,并且缓存起来,避免在没有做任何更改的情况下重复工作,提高效率。
1 增量生成器初体验
增量生成器和源代码生成器基本方法相同,只不过只需要一个Initialize方法
- 新建一个生成器项目
[Generator(LanguageNames.CSharp)]
public class GreetingIncrementalGenerator : IIncrementalGenerator
{//仅仅实现一个接口public void Initialize(IncrementalGeneratorInitializationContext context){//生成源代码的操作,不会立即执行,而是在编译时执行context.RegisterPostInitializationOutput(e =>{e.AddSource($"GreetingIncrementalGenerator.g.cs", """//加上这句话,告知编译器,这个文件是由源代码生成器生成的,//防止编译器进行代码分析,避免不必要的编译器警告//<auto-generated>namespace GreetingTest;class GreetingIncrementalGreetingIncremental{public static void SayHello(string name){global::System.Console.WriteLine($"Hello, World {name}!");}}""");});}
}
- 使用增量生成器
GreetingIncrementalGreetingIncremental.SayHello("IncrementalGeneratorInitialization");
2 使用CreateSyntaxProvider
上一章节中直接使用字符串进行了代码生成而没有进行语法分析,语法分析可采用context.RegisterSourceOutput
方法,该方法具有两个参数
- 声明一个分部类和分布方法
namespace SourceGenerator2
{public static partial class GreetingUsePartialClass{public static partial void SayHelloTo2(string name);}
}
- 新建一个类库,也就是语法生成器项目
[Generator(LanguageNames.CSharp)]
public sealed class GreetingIncrementalGenerator : IIncrementalGenerator
{public void Initialize(IncrementalGeneratorInitializationContext context){context.RegisterSourceOutput(//CreateSyntaxProvider接受一个判断方法,判断是否满足要求,并返回一个语法树context.SyntaxProvider.CreateSyntaxProvider(NodePredicate,(gsc, _) => (MethodDeclarationSyntax)gsc.Node), //第二个参数为上一步返回的经过判断的语法树(spc, method) => {var type = method.Ancestors().OfType<TypeDeclarationSyntax>().First();var typeName = type.Identifier.ValueText;spc.AddSource($"{typeName}.g.cs",$$"""//加上这句话,告知编译器,这个文件是由源代码生成器生成的,//防止编译器进行代码分析,避免不必要的编译器警告//<auto-generated>#nullable enablenamespace SourceGenerator2;partial class {{typeName}}{public static partial void SayHelloTo2(string name){global::System.Console.WriteLine($"Hello, World {name}!");}}""");});}//判断分部方法是否满足要求private static bool NodePredicate(SyntaxNode node, CancellationToken _)=> node is MethodDeclarationSyntax{Identifier.ValueText: "SayHelloTo2",Modifiers: var methodModifiers and not [],ReturnType: PredefinedTypeSyntax{Keyword.RawKind: (int)SyntaxKind.VoidKeyword},TypeParameterList: null,ParameterList.Parameters:[{Type: PredefinedTypeSyntax{Keyword.RawKind: (int)SyntaxKind.StringKeyword}}],Parent: ClassDeclarationSyntax{Modifiers: var typeModifiers and not []}}&& methodModifiers.Any(SyntaxKind.PartialKeyword)&& typeModifiers.Any(SyntaxKind.PartialKeyword)&& methodModifiers.Any(SyntaxKind.StaticKeyword);
- 使用,在主项目中输入
GreetingUsePartialClass.SayHelloTo2("SourceGenerator2");
3. 使用ForAttributeMetadataName
类似于C#源代码生成器深入讲解一中05章节所讲,如果想要借助特性来使用代码生成器,则可以使用ForAttributeMetadataName
方法
- 在主项目中声明特性
namespace SourceGenerator2
{[AttributeUsage(AttributeTargets.Method)]public sealed class SayHello2Attribute:Attribute;
}
- 在主项目中声明一个分部方法,并标记特性
namespace SourceGenerator2
{public static partial class GreetingUsePartialClass{[SayHello2]public static partial void SayHelloToAttribute(string name);}
}
- 建立代码生成器项目
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml.Linq;namespace SourceGenerator2.UseAttribute
{[Generator(LanguageNames.CSharp)]public class SourceGenerator2UseAttribute : IIncrementalGenerator{public void Initialize(IncrementalGeneratorInitializationContext context){#regioncontext.RegisterSourceOutput(//与上一章节中的区别是使用了ForAttributeWithMetadataName来创建Provider//RegisterSourceOutput第一个参数,IncrementalValueProvider<TSource>context.SyntaxProvider.ForAttributeWithMetadataName("SourceGenerator2.SayHello2Attribute",NodePredicate,static (gasc, _) => gasc switch{{TargetNode: MethodDeclarationSyntax node,TargetSymbol: IMethodSymbol{Name: var methodName,TypeParameters: [],Parameters: [{ Type.SpecialType: SpecialType.System_String, Name: var parameterName }],ReturnsVoid: true,IsStatic: true,ContainingType:{Name: var typeName,ContainingNamespace: var @namespace,TypeKind: var typeKind and (TypeKind.Class or TypeKind.Struct or TypeKind.Interface)}}} => new GatheredData{MethodName = methodName,ParameterName = parameterName,TypeName = typeName,Namespace = @namespace,TypeKind = typeKind,Node = node},_ => null}//Collect,combine等方法可对Provider进行组合).Collect(),//RegisterSourceOutput第二个参数,Action<SourceProductionContext, TSource>(spc, data) =>{foreach (var item in data){if (item is null){continue;}var namespaceName = item.Namespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);namespaceName = namespaceName["global::".Length..];var typeKindString = item.TypeKind switch{ TypeKind.Class => "class", TypeKind.Struct => "struct", TypeKind.Interface => "interface", _ => throw new NotImplementedException() };spc.AddSource($"{item.TypeName}.g.cs",$$"""// <auto-generated/>#nullable enablenamespace {{namespaceName}};partial {{typeKindString}} {{item.TypeName}}{{{item.Node.Modifiers}} void {{item.MethodName}}(string {{item.ParameterName}})=> global::System.Console.WriteLine($"Hello, {{{item.ParameterName}}}!");}""");}});#endregion}public bool NodePredicate(SyntaxNode node, CancellationToken token) => node is MethodDeclarationSyntax{Modifiers: var modifiers and not [],Parent: TypeDeclarationSyntax { Modifiers: var typemodifiers and not [] }}&& modifiers.Any(SyntaxKind.PartialKeyword)&& typemodifiers.Any(SyntaxKind.PartialKeyword);}
}file class GatheredData
{public string MethodName { set; get; }public string ParameterName { set; get; }public string TypeName { set; get; }public INamespaceSymbol Namespace { set; get; }public TypeKind TypeKind { set; get; }public MethodDeclarationSyntax Node { set; get; }
}
4. CompilationProvider和AdditionalTextsProvider
很多源代码生成器都是针对程序集的,而不是针对某个类的,所以使用CompilationProvider
[Generator(LanguageNames.CSharp)]
public class MyTupleGenerator : IIncrementalGenerator
{public void Initialize(IncrementalGeneratorInitializationContext context){//很多源代码生成器都是针对程序集的,而不是针对某个类的,所以使用CompilationProvider//因为CompilationProvider提供的功能有限,本次要使用TextsProvider,所以要使用Combine方法进行组合context.RegisterSourceOutput(context.CompilationProvider.Combine(context.AdditionalTextsProvider.Collect()), Output);}private void Output(SourceProductionContext spc, (Compilation, ImmutableArray<AdditionalText>) pair){var (compilation, additionalFiles) = pair;spc.AddSource("mytuble.g.cs","source...省略");}
}