首页 > 其他分享 >源生成器:根据需要自动生成机械重复代码

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

时间:2024-01-14 21:14:34浏览次数:29  
标签:string DependencyProperty 重复 Attribute 代码 生成器 DependencyPropertyAttribute public

title: 源生成器:根据需要自动生成机械重复代码
date: 2022-02-02
tags:
- C#
- .NET
- Roslyn

前言

本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附带的源生成器(Source Generator)自动生成机械重复的代码。关于这部分的基础入门知识可以在MSDN[1]学到。

本文默认已经有一个解决方案,包含两个项目。一个是普通C#项目,依赖于另一个源生成器项目。

创建及使用Attribute

此处以DependencyPropertyAttribute为例,可以为拥有本Attribute的类,自动获取所有定义过的属性,并将它们在一个构造函数里初始化。

DependencyProperty的名称、类型、属性改变处理函数都是必须指定的,可选指定内容是属性setter的公共性、该类型的null性、和默认值。可选内容有默认值。

以下是DependencyPropertyAttribute的实现:

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
{
    ...
}

这将会生成如下代码:

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中,只会用到构造函数和可选指定内容的属性,这说明实现可以简化为:

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增量生成器。旧的源生成器在每次代码有更改时都会扫描整个语法树,开销很大,新的增量生成器[3]通过管道[4]等方式遴选需要扫描的代码,大大减少生成开销。增量生成器是Roslyn 4.0的新功能,对应VS17.0(即Visual Studio 2022),也就是说只有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; }

    // 注:由于我写的所有`Attribute`都是用的同一个命名空间,
    // 所以可以通过组合`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;

            // 注:此处我指定了一个特殊的`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,这个方法可以根据所给的名字,找到所有拥有该Attribute的单元,用它写的代码比之前简洁太多了,现在介绍一下这个方法:

它的第一个参数是:

string fullyQualifiedMetadataName

输入Attribute的元数据全名即可,如果是泛型则应该写为类似这样的形式:

"Attributes.DependencyPropertyAttribute`1"

第二个参数是一个委托:

Func<Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken, bool> predicate

提供对应class、property等拥有指定Attribute的单元(以下简称“目标单元”)的语法节点和取消标识,返回一个bool表示是否保留这项,一般直接返回true即可。

第三个参数也是委托:

Func<Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext, System.Threading.CancellationToken, T> transform

提供目标单元的一个“生成器特性语法上下文(GeneratorAttributeSyntaxContext)”和取消标识,返回你想保留的、关于这个单元的数据,一般直接返回GeneratorAttributeSyntaxContext参数即可。

这个GeneratorAttributeSyntaxContext十分好用,他有四个属性,都是我们需要的:

第一个是目标节点,即目标单元的语法树,一般是TypeDeclarationSyntax的子类

SyntaxNode TargetNode

第二个是目标符号,一般是INamedTypeSymbol或IPropertySymbol等

ISymbol TargetSymbol

第三个是语义模型,即目标单元所在文件的语法树

SemanticModel SemanticModel

第四个是特性数组,是目标单元上所有的指定Attribute

ImmutableArray<AttributeData> Attributes

原来这些数据都需要我们在Execute中自己收集,而现在微软已经全部封装好了。

实现生成器

接下来我们通过继承来实现生成器:

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。如:

[DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]

这种写法其实是一个构造函数,只是不像普通的类型那样用new而已。所以获取DependencyPropertyAttribute的参数只需要分析他的构造函数即可:

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;
                }
        
        ...
    }
}

这便是分析一个构造函数的代码了,还比较简短吧?
这块代码其实主要分为三个部分,我们可以以这句为例分析一下:

[DependencyProperty<string>("Name", nameof(Method), IsNullable = true)]

第一部分:这块是获取泛型参数,即<string>。如果没有泛型参数肯定是错误的,所以直接返回空值。

if (attribute.AttributeClass is not { TypeArguments: [var type, ..] })
    return null;

第二部分:这块是获取构造函数的参数,即"Name", nameof(Method)部分。注意如果就算使用了缺省参数的话,它的值也是可以在这里捕捉到的。如果有多个构造函数的话简单替换为switch语句即可。

if (attribute.ConstructorArguments is not
    [
        { Value: string propertyName },
        { Value: string defaultValue },
        { Value: string propertyChanged },
        ..
    ])
    continue;

第三部分:这块是获取初始化列表,即IsNullable = true。这里的赋值是在执行完构造函数之后才会发生,所以严格来说其实不是构造函数的一部分,但我们确实可以获得执行参数。注意这里和上面不一样,如果没有指定这些参数的话,这里就捕捉不到,所以我们不能获取不到就返回空值了,而要直接给参数赋值为默认值。

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输入和程序原本的逻辑拼接字符串,最后将拼接成的字符串源码返回,即可成功运行了!折腾字符串的部分就不仔细介绍了,大家有兴趣可以看我的仓库[6]


  1. Source Generators ↩︎

  2. Generic Attributes ↩︎

  3. GitHub-IncrementalGenerators ↩︎

  4. Creating a source generator ↩︎

  5. SyntaxValueProvider.ForAttributeWithMetadataName Method ↩︎

  6. WinUI3Utilities ↩︎

标签:string,DependencyProperty,重复,Attribute,代码,生成器,DependencyPropertyAttribute,public
From: https://www.cnblogs.com/pokersang/p/17964185

相关文章

  • C#与C++代码的互操作方式
    title:C#与C++代码的互操作方式date:2024-01-10categories:编程tags:-C#-.NET-C++-COM-平台调用大致介绍在写C#程序时经常有与本地代码(C/C++)代码交互的需求。微软提供了许多种方式供我们选择,最常用的有以下三种(A->B指A可以引用B):flowchartLRA--P/Invoke......
  • 折腾物料代码
    问题:物料代码最后点后是三位数保留原状,最后点后是有效四位数保留原状,后四位不满千的保留三位。函数公式解决:=TEXTBEFORE(C2,".",-1)&"."&TEXT(TEXTAFTER(C2,".",-1),"000")TextBefore():提取从右往左数第一个点之前的内容&"."&:与点连接,再连接后面的内容Text():将数字格式......
  • php代码审计(三)bluecms练习
    BlueCMSv1.6sp1ad_js.phpSQL注入漏洞环境搭建源码下载:https://jwt1399.lanzoui.com/inPwSde6c5a将upload下的文件移动到网站根目录下访问/install,安装程序,配置基本信息seay自动审计定位到第一条的位置/ad_js.php全代码通读,首先是包含了文件/include/common.inc.php,查看......
  • JUC并发编程 CompletableFuture 业务代码实战
    1需求电商网站比价需求分析:1.1需求说明:a.同一款产品,同时搜索出同款产品在各大电商平台的售价b.同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少1.2输出返回:a.出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List例如:《Mysql》......
  • 【代码复现(吐槽向)】Revisiting a Methodology for Efficient CNN Architectures in Pr
    【论文写不出来,痛苦中】这篇文章是我看到框架最简单,效果最好的对于公开数据集的攻击没有之一。代码:KULeuven-COSIC/TCHES20V3_CNN_SCA(github.com)吐槽:1坑:TF的版本问题,有了torch,谁用TF,但是偏偏GITHUB上所有的SCA的代码都是TF写的,还有丧心病狂TF1.x,版本安装几十年,不如选一个服......
  • [刷题班] LeetCode80. 删除有序数组中的重复项II
    题目描述思路:快慢指针slow指针指向已经处理元素的下一个位置因为数组有序,如果nums[fast]==nums[slow-2],那么nums[fast]肯定等于nums[slow-1],那么此时这个数就出现了三次。此时slow保持不变,fast继续遍历。关键:nums[fast]!=nums[slow-2]方法一:classSolution{......
  • [刷题班] LeetCode26. 删除有序数组中的重复项
    题目描述思路:快慢指针slow指针:指向已经处理的区域(没有重复元素)的最后一个位置fast指针:指向当前正在处理的元素方法一:classSolution{publicintremoveDuplicates(int[]nums){intslow=0,fast=0;for(;fast<nums.length;fast++){......
  • # yyds干货盘点 # 盘点一个AI解答疑难代码的问题
    大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas代码解读的问题,问题如下:df_in=df[df['入/出'].eq('入')],我也不懂eq啥意思?感觉这代码还可以写成df[df['入/出']=='入'],这两段一个意思吧。答:eq就是=,就是你说的这个。二、实现过程后来【论草莓如......
  • 重复的子字符串
    最开始想的是暴力解法,但总是超时,后来问了chatgp,可以通过用substr来缩短时间。勉强通过,耗时还是很大。点击查看代码classSolution{public:boolrepeatedSubstringPattern(strings){intcount=1;stringtemp;while(count<=s.size()/2){temp=s.substr(0,count);......
  • 使用RanDom生成不重复的随机数
    首先看一下关键词的傻瓜讲解Random用法Random.Next()返回非负随机数;Random.Next(a)返回一个小于a的非负随机数Random.Next(a,b)返回一个大于a小于b的非负随机数contains用法list.Contains(a)判断列表list里是否含有a,有则返回true接下来看代码staticvoidMain(string[]args)......