title: 源生成器:根據(jù)需要自動(dòng)生成機(jī)械重復(fù)代碼
date: 2022-02-02
tags:
- C#
- .NET
- Roslyn
前言
本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附帶的源生成器(Source Generator)自動(dòng)生成機(jī)械重復(fù)的代碼。關(guān)于這部分的基礎(chǔ)入門(mén)知識(shí)可以在MSDN[1]學(xué)到。
本文默認(rèn)已經(jīng)有一個(gè)解決方案,包含兩個(gè)項(xiàng)目。一個(gè)是普通C#項(xiàng)目,依賴于另一個(gè)源生成器項(xiàng)目。
創(chuàng)建及使用Attribute
此處以DependencyPropertyAttribute
為例,可以為擁有本Attribute
的類,自動(dòng)獲取所有定義過(guò)的屬性,并將它們?cè)谝粋€(gè)構(gòu)造函數(shù)里初始化。
本DependencyProperty
的名稱、類型、屬性改變處理函數(shù)都是必須指定的,可選指定內(nèi)容是屬性setter的公共性、該類型的null性、和默認(rèn)值??蛇x內(nèi)容有默認(rèn)值。
以下是DependencyPropertyAttribute
的實(shí)現(xiàn):
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[2]),所以此處我們直接使用泛型。
以下是使用示例:
namespace Controls.IconButton;
[DependencyProperty<string>("Text", nameof(OnTextChanged))]
[DependencyProperty<IconElement>("Icon", nameof(OnIconChanged))]
public partial class IconButton : Button
{
...
}
這將會(huì)生成如下代碼:
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
中建議只使用基本類型的常量,因?yàn)閺?fù)雜類型不方便獲取。
注:被添加Attribute
的類(如IconButton
)要加partial
關(guān)鍵字,否則會(huì)出重定義錯(cuò)誤。
注:DependencyPropertyAttribute
中,只會(huì)用到構(gòu)造函數(shù)和可選指定內(nèi)容的屬性,這說(shuō)明實(shí)現(xiàn)可以簡(jiǎn)化為:
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; }
}
因?yàn)楫?dāng)源生成器分析的時(shí)候,分析的是被捕獲的類(如IconButton)及其上下文,而非DependencyPropertyAttribute
的,所以其他內(nèi)容實(shí)際上用不上。
但原來(lái)的寫(xiě)法方便將來(lái)可能需要反射本Attribute
的操作,也方便閱讀,所以建議保留。
創(chuàng)建通用基類
類TypeWithAttributeGenerator
可以作為所有分析類型上的Attribute
的分析器的模板基類。繼承它后只需傳入AttributeName
便可以自動(dòng)執(zhí)行對(duì)應(yīng)方法了。
除了屬性AttributeName
外,還有一個(gè)需要子類實(shí)現(xiàn)的是方法TypeWithAttribute
。它傳入的參數(shù)分別是Attribute
所在的類型和它所擁有的所有指定Attribute
,可能有多個(gè)所以是數(shù)組。這個(gè)方法返回的就是生成的文件代碼,以string
傳回;如果中途發(fā)生任何錯(cuò)誤無(wú)法生成,則返回null
即可。
此處我們使用的是IIncrementalGenerator
增量生成器。舊的源生成器在每次代碼有更改時(shí)都會(huì)掃描整個(gè)語(yǔ)法樹(shù),開(kāi)銷很大,新的增量生成器[3]通過(guò)管道[4]等方式遴選需要掃描的代碼,大大減少生成開(kāi)銷。增量生成器是Roslyn 4.0的新功能,對(duì)應(yīng)VS17.0(即Visual Studio?2022),也就是說(shuō)只有VS2022及以上的版本才可以使用。
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; }
// 注:由于我寫(xiě)的所有`Attribute`都是用的同一個(gè)命名空間,
// 所以可以通過(guò)組合`AttributeNamespace`和`AttributeName`便可以得到完整名稱。
// `AttributeNamespace`為"Attributes."
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;
// 注:此處我指定了一個(gè)特殊的`Attribute`,如果使用了它就禁用所有源生成器。
// 如:[assembly: DisableSourceGenerator]
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>
[5]是Roslyn 4.3.0新提供的API,這個(gè)方法可以根據(jù)所給的名字,找到所有擁有該Attribute
的單元,用它寫(xiě)的代碼比之前簡(jiǎn)潔太多了,現(xiàn)在介紹一下這個(gè)方法:
它的第一個(gè)參數(shù)是:
string fullyQualifiedMetadataName
輸入Attribute的元數(shù)據(jù)全名即可,如果是泛型則應(yīng)該寫(xiě)為類似這樣的形式:
"Attributes.DependencyPropertyAttribute`1"
第二個(gè)參數(shù)是一個(gè)委托:
Func<Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken, bool> predicate
提供對(duì)應(yīng)class、property等擁有指定Attribute的單元(以下簡(jiǎn)稱“目標(biāo)單元”)的語(yǔ)法節(jié)點(diǎn)和取消標(biāo)識(shí),返回一個(gè)bool表示是否保留這項(xiàng),一般直接返回true即可。
第三個(gè)參數(shù)也是委托:
Func<Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext, System.Threading.CancellationToken, T> transform
提供目標(biāo)單元的一個(gè)“生成器特性語(yǔ)法上下文(GeneratorAttributeSyntaxContext)”和取消標(biāo)識(shí),返回你想保留的、關(guān)于這個(gè)單元的數(shù)據(jù),一般直接返回GeneratorAttributeSyntaxContext
參數(shù)即可。
這個(gè)GeneratorAttributeSyntaxContext
十分好用,他有四個(gè)屬性,都是我們需要的:
第一個(gè)是目標(biāo)節(jié)點(diǎn),即目標(biāo)單元的語(yǔ)法樹(shù),一般是TypeDeclarationSyntax
的子類
SyntaxNode TargetNode
第二個(gè)是目標(biāo)符號(hào),一般是INamedTypeSymbol或IPropertySymbol等
ISymbol TargetSymbol
第三個(gè)是語(yǔ)義模型,即目標(biāo)單元所在文件的語(yǔ)法樹(shù)
SemanticModel SemanticModel
第四個(gè)是特性數(shù)組,是目標(biāo)單元上所有的指定Attribute
ImmutableArray<AttributeData> Attributes
原來(lái)這些數(shù)據(jù)都需要我們?cè)贓xecute中自己收集,而現(xiàn)在微軟已經(jīng)全部封裝好了。
實(shí)現(xiàn)生成器
接下來(lái)我們通過(guò)繼承來(lái)實(shí)現(xiàn)生成器:
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)
{
...
}
}
我們主要說(shuō)一下如何獲取類型上的Attribute
。如:
[DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]
這種寫(xiě)法其實(shí)是一個(gè)構(gòu)造函數(shù),只是不像普通的類型那樣用new
而已。所以獲取DependencyPropertyAttribute
的參數(shù)只需要分析他的構(gòu)造函數(shù)即可:
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;
}
...
}
}
這便是分析一個(gè)構(gòu)造函數(shù)的代碼了,還比較簡(jiǎn)短吧?
這塊代碼其實(shí)主要分為三個(gè)部分,我們可以以這句為例分析一下:
[DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]
第一部分:這塊是獲取泛型參數(shù),即<string>
。如果沒(méi)有泛型參數(shù)肯定是錯(cuò)誤的,所以直接返回空值。
if (attribute.AttributeClass is not { TypeArguments: [var type, ..] })
return null;
第二部分:這塊是獲取構(gòu)造函數(shù)的參數(shù),即"Name", nameof(Method)
部分。注意如果就算使用了缺省參數(shù)的話,它的值也是可以在這里捕捉到的。如果有多個(gè)構(gòu)造函數(shù)的話簡(jiǎn)單替換為switch
語(yǔ)句即可。
if (attribute.ConstructorArguments is not
[
{ Value: string propertyName },
{ Value: string defaultValue },
{ Value: string propertyChanged },
..
])
continue;
第三部分:這塊是獲取初始化列表,即IsNullable = true
。這里的賦值是在執(zhí)行完構(gòu)造函數(shù)之后才會(huì)發(fā)生,所以嚴(yán)格來(lái)說(shuō)其實(shí)不是構(gòu)造函數(shù)的一部分,但我們確實(shí)可以獲得執(zhí)行參數(shù)。注意這里和上面不一樣,如果沒(méi)有指定這些參數(shù)的話,這里就捕捉不到,所以我們不能獲取不到就返回空值了,而要直接給參數(shù)賦值為默認(rèn)值。
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;
}
以上是分析構(gòu)造函數(shù)的部分,接下來(lái)就是絕大部分程序員的老本行:折騰字符串了。根據(jù)Attribute
輸入和程序原本的邏輯拼接字符串,最后將拼接成的字符串源碼返回,即可成功運(yùn)行了!折騰字符串的部分就不仔細(xì)介紹了,大家有興趣可以看我的倉(cāng)庫(kù)[6]。
-
Source Generators ??
-
Generic Attributes ??
-
GitHub-IncrementalGenerators ??
-
Creating a source generator ??
-
SyntaxValueProvider.ForAttributeWithMetadataName
Method ??文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-788592.html -
WinUI3Utilities ??文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-788592.html
到了這里,關(guān)于源生成器:根據(jù)需要自動(dòng)生成機(jī)械重復(fù)代碼的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!