前言
本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附带的源生成器 (Source Generator)自动生成机械重复的代码。关于这部分的基础入门知识可以在MSDN学到。
本文默认 已经有一个解决方案,包含两个项目。一个是普通C#项目,依赖于另一个源生成器项目。
创建及使用Attribute
此处以DependencyPropertyAttribute
为例,可以为拥有本Attribute
的类,自动获取所有定义过的属性,并将它们在一个构造函数里初始化。
本DependencyProperty
的名称、类型、属性改变处理函数都是必须指定的,可选指定内容是属性setter的公共性、该类型的null性、和默认值。可选内容有默认值。
以下是DependencyPropertyAttribute
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System;namespace Attributes ;[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false) ] public sealed class DependencyPropertyAttribute <T > : Attribute where T : notnull { public DependencyPropertyAttribute (string name, string propertyChanged = "" ) { Name = name; PropertyChanged = propertyChanged; } public string Name { get ; } public string PropertyChanged { get ; } public bool IsSetterPublic { get ; init ; } = true ; public bool IsNullable { get ; init ; } = true ; public string DefaultValue { get ; init ; } = "DependencyProperty.UnsetValue" ; }
在.NET 7中,加入了新的泛型特性(Generic Attributes),所以此处我们直接使用泛型。
以下是使用示例:
1 2 3 4 5 6 7 8 namespace Controls.IconButton ;[DependencyProperty<string>("Text" , nameof(OnTextChanged)) ] [DependencyProperty<IconElement>("Icon" , nameof(OnIconChanged)) ] public partial class IconButton : Button { ... }
这将会生成如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using Microsoft.UI.Xaml;using System;using Microsoft.UI.Xaml.Controls;#nullable enable namespace Controls.IconButton { partial class IconButton { public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text" , typeof (string ), typeof (IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnTextChanged)); public string Text { get => (string )GetValue(TextProperty); set => SetValue(TextProperty, value ); } public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon" , typeof (IconElement), typeof (IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnIconChanged)); public IconElement Icon { get => (IconElement)GetValue(IconProperty); set => SetValue(IconProperty, value ); } } }
注:DependencyPropertyAttribute
中建议只使用基本类型的常量,因为复杂类型不方便获取。
注:被添加Attribute
的类(如IconButton
)要加partial
关键字,否则会出重定义错误。
注:DependencyPropertyAttribute
中,只会用到构造函数和可选指定内容的属性,这说明实现可以简化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System;namespace Attributes ;[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false) ] public sealed class DependencyPropertyAttribute <T > : Attribute where T : notnull { public DependencyPropertyAttribute (string name, string propertyChanged = "" ) { } public bool IsSetterPublic { get ; init ; } public bool IsNullable { get ; init ; } public string DefaultValue { get ; init ; } }
因为当源生成器分析的时候,分析的是被捕获的类(如IconButton)及其上下文,而非DependencyPropertyAttribute
的,所以其他内容实际上用不上。
但原来的写法方便将来可能需要反射本Attribute
的操作,也方便阅读,所以建议保留。
创建通用基类
类TypeWithAttributeGenerator
可以作为所有分析类型上的Attribute
的分析器的模板基类。继承它后只需传入AttributeName
便可以自动执行对应方法了。
除了属性AttributeName
外,还有一个需要子类实现的是方法TypeWithAttribute
。它传入的参数分别是Attribute
所在的类型和它所拥有的所有指定Attribute
,可能有多个所以是数组。这个方法返回的就是生成的文件代码,以string
传回;如果中途发生任何错误无法生成,则返回null
即可。
此处我们使用的是IIncrementalGenerator
增量生成器。旧的源生成器在每次代码有更改时都会扫描整个语法树,开销很大,新的增量生成器通过管道等方式遴选需要扫描的代码,大大减少生成开销。增量生成器是Roslyn 4.0的新功能,对应VS17.0(即Visual Studio 2022),也就是说只有VS2022及以上的版本才可以使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 using System.Collections.Immutable;using System.Linq;using Microsoft.CodeAnalysis;using static SourceGenerator.Utilities.SourceGeneratorHelper;namespace SourceGenerator ;public abstract class TypeWithAttributeGenerator : IIncrementalGenerator { internal abstract string AttributeName { get ; } private string AttributeFullName => AttributeNamespace + AttributeName; internal abstract string ? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList); public void Initialize (IncrementalGeneratorInitializationContext context ) { var generatorAttributes = context.SyntaxProvider.ForAttributeWithMetadataName( AttributeFullName, (_, _) => true , (syntaxContext, _) => syntaxContext ).Combine(context.CompilationProvider); context.RegisterSourceOutput(generatorAttributes, (spc, tuple) => { var (ga, compilation) = tuple; if (compilation.Assembly.GetAttributes().Any(attrData => attrData.AttributeClass?.ToDisplayString() == DisableSourceGeneratorAttribute)) return ; if (ga.TargetSymbol is not INamedTypeSymbol symbol) return ; if (TypeWithAttribute(symbol, ga.Attributes) is { } source) spc.AddSource( $"{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))} _{AttributeFullName} .g.cs" , source); }); } }
获取特性的重要方法
ForAttributeWithMetadataName<T>
是Roslyn 4.3.0新提供的API,这个方法可以根据所给的名字,找到所有拥有该Attribute
的单元,用它写的代码比之前简洁太多了,现在介绍一下这个方法:
它的第一个参数是:
1 string fullyQualifiedMetadataName
输入Attribute的元数据全名即可,如果是泛型则应该写为类似这样的形式:
1 "Attributes.DependencyPropertyAttribute`1"
第二个参数是一个委托:
1 Func<Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken, bool > predicate
提供对应class、property等拥有指定Attribute的单元(以下简称“目标单元”)的语法节点和取消标识,返回一个bool表示是否保留这项,一般直接返回true即可。
第三个参数也是委托:
1 Func<Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext, System.Threading.CancellationToken, T> transform
提供目标单元的一个“生成器特性语法上下文(GeneratorAttributeSyntaxContext)”和取消标识,返回你想保留的、关于这个单元的数据,一般直接返回GeneratorAttributeSyntaxContext
参数即可。
这个GeneratorAttributeSyntaxContext
十分好用,他有四个属性,都是我们需要的:
第一个是目标节点,即目标单元的语法树,一般是TypeDeclarationSyntax
的子类
第二个是目标符号,一般是INamedTypeSymbol或IPropertySymbol等
第三个是语义模型,即目标单元所在文件的语法树
1 SemanticModel SemanticModel
第四个是特性数组,是目标单元上所有的指定Attribute
1 ImmutableArray<AttributeData> Attributes
原来这些数据都需要我们在Execute中自己收集,而现在微软已经全部封装好了。
实现生成器
接下来我们通过继承来实现生成器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System.Collections.Immutable;using Microsoft.CodeAnalysis;namespace SourceGenerator ;[Generator ] public class DependencyPropertyGenerator : TypeWithAttributeGenerator { internal override string AttributeName => "DependencyPropertyAttribute`1" ; internal override string ? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList) { ... } }
我们主要说一下如何获取类型上的Attribute
。如:
1 [DependencyProperty<string>("Name" , nameof(Method), IsNullable = true) ]
这种写法其实是一个构造函数,只是不像普通的类型那样用new
而已。所以获取DependencyPropertyAttribute
的参数只需要分析他的构造函数即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 internal override string ? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList){ foreach (var attribute in attributeList) { if (attribute.AttributeClass is not { TypeArguments: [var type, ..] }) return null ; if (attribute.ConstructorArguments is not [ { Value: string propertyName }, { Value: string defaultValue }, { Value: string propertyChanged }, .. ]) continue ; var isSetterPrivate = false ; var isNullable = false ; foreach (var namedArgument in attribute.NamedArguments) if (namedArgument.Value.Value is { } value ) switch (namedArgument.Key) { case "IsSetterPrivate" : isSetterPrivate = (bool )value ; break ; case "IsNullable" : isNullable = (bool )value ; break ; } ... } }
这便是分析一个构造函数的代码了,还比较简短吧? 这块代码其实主要分为三个部分,我们可以以这句为例分析一下:
1 [DependencyProperty<string>("Name" , nameof(Method), IsNullable = true) ]
第一部分:这块是获取泛型参数,即<string>
。如果没有泛型参数肯定是错误的,所以直接返回空值。
1 2 if (attribute.AttributeClass is not { TypeArguments: [var type, ..] }) return null ;
第二部分:这块是获取构造函数的参数,即"Name", nameof(Method)
部分。注意如果就算使用了缺省参数的话,它的值也是可以在这里捕捉到的。如果有多个构造函数的话简单替换为switch
语句即可。
1 2 3 4 5 6 7 8 if (attribute.ConstructorArguments is not [ { Value: string propertyName }, { Value: string defaultValue }, { Value: string propertyChanged }, .. ]) continue ;
第三部分:这块是获取初始化列表,即IsNullable = true
。这里的赋值是在执行完构造函数之后才会发生,所以严格来说其实不是构造函数的一部分,但我们确实可以获得执行参数。注意这里和上面不一样,如果没有指定这些参数的话,这里就捕捉不到,所以我们不能获取不到就返回空值了,而要直接给参数赋值为默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var isSetterPrivate = false ;var isNullable = false ;foreach (var namedArgument in attribute.NamedArguments) if (namedArgument.Value.Value is { } value ) switch (namedArgument.Key) { case "IsSetterPrivate" : isSetterPrivate = (bool )value ; break ; case "IsNullable" : isNullable = (bool )value ; break ; }
以上是分析构造函数的部分,接下来就是绝大部分程序员的老本行:折腾字符串了。根据Attribute
输入和程序原本的逻辑拼接字符串,最后将拼接成的字符串源码返回,即可成功运行了!折腾字符串的部分就不仔细介绍了,大家有兴趣可以看我的仓库。