翼度科技»论坛 编程开发 .net 查看内容

源生成器:根据需要自动生成机械重复代码

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
  1. title: 源生成器:根据需要自动生成机械重复代码
  2. date: 2022-02-02
  3. tags:
  4. - C#
  5. - .NET
  6. - Roslyn
复制代码
前言

本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附带的源生成器(Source Generator)自动生成机械重复的代码。关于这部分的基础入门知识可以在MSDN[1]学到。
本文默认已经有一个解决方案,包含两个项目。一个是普通C#项目,依赖于另一个源生成器项目。
创建及使用Attribute

此处以DependencyPropertyAttribute为例,可以为拥有本Attribute的类,自动获取所有定义过的属性,并将它们在一个构造函数里初始化。
本DependencyProperty的名称、类型、属性改变处理函数都是必须指定的,可选指定内容是属性setter的公共性、该类型的null性、和默认值。可选内容有默认值。
以下是DependencyPropertyAttribute的实现:
  1. using System;
  2. namespace Attributes;
  3. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
  4. public sealed class DependencyPropertyAttribute<T> : Attribute where T : notnull
  5. {
  6.     public DependencyPropertyAttribute(string name, string propertyChanged = "")
  7.     {
  8.         Name = name;
  9.         PropertyChanged = propertyChanged;
  10.     }
  11.     public string Name { get; }
  12.     public string PropertyChanged { get; }
  13.     public bool IsSetterPublic { get; init; } = true;
  14.     public bool IsNullable { get; init; } = true;
  15.     public string DefaultValue { get; init; } = "DependencyProperty.UnsetValue";
  16. }
复制代码
在.NET 7中,加入了新的泛型特性(Generic Attributes[2]),所以此处我们直接使用泛型。
以下是使用示例:
  1. namespace Controls.IconButton;
  2. [DependencyProperty<string>("Text", nameof(OnTextChanged))]
  3. [DependencyProperty<IconElement>("Icon", nameof(OnIconChanged))]
  4. public partial class IconButton : Button
  5. {
  6.     ...
  7. }
复制代码
这将会生成如下代码:
  1. using Microsoft.UI.Xaml;
  2. using System;
  3. using Microsoft.UI.Xaml.Controls;
  4. #nullable enable
  5. namespace Controls.IconButton
  6. {
  7.     partial class IconButton
  8.     {
  9.         public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnTextChanged));
  10.         public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
  11.         public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(IconElement), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnIconChanged));
  12.         public IconElement Icon { get => (IconElement)GetValue(IconProperty); set => SetValue(IconProperty, value); }
  13.     }
  14. }
复制代码
注:DependencyPropertyAttribute中建议只使用基本类型的常量,因为复杂类型不方便获取。
注:被添加Attribute的类(如IconButton)要加partial关键字,否则会出重定义错误。
注:DependencyPropertyAttribute中,只会用到构造函数和可选指定内容的属性,这说明实现可以简化为:
  1. using System;
  2. namespace Attributes;
  3. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
  4. public sealed class DependencyPropertyAttribute<T> : Attribute where T : notnull
  5. {
  6.     public DependencyPropertyAttribute(string name, string propertyChanged = "") { }
  7.     public bool IsSetterPublic { get; init; }
  8.     public bool IsNullable { get; init; }
  9.     public string DefaultValue { get; init; }
  10. }
复制代码
因为当源生成器分析的时候,分析的是被捕获的类(如IconButton)及其上下文,而非DependencyPropertyAttribute的,所以其他内容实际上用不上。
但原来的写法方便将来可能需要反射本Attribute的操作,也方便阅读,所以建议保留。
创建通用基类

类TypeWithAttributeGenerator可以作为所有分析类型上的Attribute的分析器的模板基类。继承它后只需传入AttributeName便可以自动执行对应方法了。
除了属性AttributeName外,还有一个需要子类实现的是方法TypeWithAttribute。它传入的参数分别是Attribute所在的类型和它所拥有的所有指定Attribute,可能有多个所以是数组。这个方法返回的就是生成的文件代码,以string传回;如果中途发生任何错误无法生成,则返回null即可。
此处我们使用的是IIncrementalGenerator增量生成器。旧的源生成器在每次代码有更改时都会扫描整个语法树,开销很大,新的增量生成器[3]通过管道[4]等方式遴选需要扫描的代码,大大减少生成开销。增量生成器是Roslyn 4.0的新功能,对应VS17.0(即Visual Studio 2022),也就是说只有VS2022及以上的版本才可以使用。
  1. using System.Collections.Immutable;
  2. using System.Linq;
  3. using Microsoft.CodeAnalysis;
  4. using static SourceGenerator.Utilities.SourceGeneratorHelper;
  5. namespace SourceGenerator;
  6. public abstract class TypeWithAttributeGenerator : IIncrementalGenerator
  7. {
  8.     internal abstract string AttributeName { get; }
  9.     // 注:由于我写的所有`Attribute`都是用的同一个命名空间,
  10.     // 所以可以通过组合`AttributeNamespace`和`AttributeName`便可以得到完整名称。
  11.     // `AttributeNamespace`为"Attributes."
  12.     private string AttributeFullName => AttributeNamespace + AttributeName;
  13.     internal abstract string? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList);
  14.     public void Initialize(IncrementalGeneratorInitializationContext context)
  15.     {
  16.         var generatorAttributes = context.SyntaxProvider.ForAttributeWithMetadataName(
  17.             AttributeFullName,
  18.             (_, _) => true,
  19.             (syntaxContext, _) => syntaxContext
  20.         ).Combine(context.CompilationProvider);
  21.         context.RegisterSourceOutput(generatorAttributes, (spc, tuple) =>
  22.         {
  23.             var (ga, compilation) = tuple;
  24.             // 注:此处我指定了一个特殊的`Attribute`,如果使用了它就禁用所有源生成器。
  25.             // 如:[assembly: DisableSourceGenerator]
  26.             if (compilation.Assembly.GetAttributes().Any(attrData => attrData.AttributeClass?.ToDisplayString() == DisableSourceGeneratorAttribute))
  27.                 return;
  28.             if (ga.TargetSymbol is not INamedTypeSymbol symbol)
  29.                 return;
  30.             if (TypeWithAttribute(symbol, ga.Attributes) is { } source)
  31.                 spc.AddSource(
  32.                     // 不能重名
  33.                     $"{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}_{AttributeFullName}.g.cs",
  34.                     source);
  35.         });
  36.     }
  37. }
复制代码
获取特性的重要方法

ForAttributeWithMetadataName[5]是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的子类
  1. SyntaxNode TargetNode
复制代码
第二个是目标符号,一般是INamedTypeSymbol或IPropertySymbol等
  1. ISymbol TargetSymbol
复制代码
第三个是语义模型,即目标单元所在文件的语法树
  1. SemanticModel SemanticModel
复制代码
第四个是特性数组,是目标单元上所有的指定Attribute
  1. ImmutableArray<AttributeData> Attributes
复制代码
原来这些数据都需要我们在Execute中自己收集,而现在微软已经全部封装好了。
实现生成器

接下来我们通过继承来实现生成器:
  1. using System.Collections.Immutable;
  2. using Microsoft.CodeAnalysis;
  3. namespace SourceGenerator;
  4. [Generator]
  5. public class DependencyPropertyGenerator : TypeWithAttributeGenerator
  6. {
  7.     internal override string AttributeName => "DependencyPropertyAttribute`1";
  8.     internal override string? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList)
  9.     {
  10.         ...
  11.     }
  12. }
复制代码
我们主要说一下如何获取类型上的Attribute。如:
  1. [DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]
复制代码
这种写法其实是一个构造函数,只是不像普通的类型那样用new而已。所以获取DependencyPropertyAttribute的参数只需要分析他的构造函数即可:
  1. internal override string? TypeWithAttribute(INamedTypeSymbol typeSymbol, ImmutableArray<AttributeData> attributeList)
  2. {
  3.     foreach (var attribute in attributeList)
  4.     {
  5.         if (attribute.AttributeClass is not { TypeArguments: [var type, ..] })
  6.             return null;
  7.         if (attribute.ConstructorArguments is not
  8.             [
  9.                 { Value: string propertyName },
  10.                 { Value: string defaultValue },
  11.                 { Value: string propertyChanged },
  12.                 ..
  13.             ])
  14.             continue;
  15.         var isSetterPrivate = false;
  16.         var isNullable = false;
  17.         foreach (var namedArgument in attribute.NamedArguments)
  18.             if (namedArgument.Value.Value is { } value)
  19.                 switch (namedArgument.Key)
  20.                 {
  21.                     case "IsSetterPrivate":
  22.                         isSetterPrivate = (bool)value;
  23.                         break;
  24.                     case "IsNullable":
  25.                         isNullable = (bool)value;
  26.                         break;
  27.                 }
  28.         
  29.         ...
  30.     }
  31. }
复制代码
这便是分析一个构造函数的代码了,还比较简短吧?
这块代码其实主要分为三个部分,我们可以以这句为例分析一下:
  1. [DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]
复制代码
第一部分:这块是获取泛型参数,即。如果没有泛型参数肯定是错误的,所以直接返回空值。
  1. if (attribute.AttributeClass is not { TypeArguments: [var type, ..] })
  2.     return null;
复制代码
第二部分:这块是获取构造函数的参数,即"Name", nameof(Method)部分。注意如果就算使用了缺省参数的话,它的值也是可以在这里捕捉到的。如果有多个构造函数的话简单替换为switch语句即可。
  1. if (attribute.ConstructorArguments is not
  2.     [
  3.         { Value: string propertyName },
  4.         { Value: string defaultValue },
  5.         { Value: string propertyChanged },
  6.         ..
  7.     ])
  8.     continue;
复制代码
第三部分:这块是获取初始化列表,即IsNullable = true。这里的赋值是在执行完构造函数之后才会发生,所以严格来说其实不是构造函数的一部分,但我们确实可以获得执行参数。注意这里和上面不一样,如果没有指定这些参数的话,这里就捕捉不到,所以我们不能获取不到就返回空值了,而要直接给参数赋值为默认值。
  1. var isSetterPrivate = false;
  2. var isNullable = false;
  3. foreach (var namedArgument in attribute.NamedArguments)
  4.     if (namedArgument.Value.Value is { } value)
  5.         switch (namedArgument.Key)
  6.         {
  7.             case "IsSetterPrivate":
  8.                 isSetterPrivate = (bool)value;
  9.                 break;
  10.             case "IsNullable":
  11.                 isNullable = (bool)value;
  12.                 break;
  13.         }
复制代码
以上是分析构造函数的部分,接下来就是绝大部分程序员的老本行:折腾字符串了。根据Attribute输入和程序原本的逻辑拼接字符串,最后将拼接成的字符串源码返回,即可成功运行了!折腾字符串的部分就不仔细介绍了,大家有兴趣可以看我的仓库[6]


来源:https://www.cnblogs.com/pokersang/Undeclared/17964185
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具