C# 9 新特性:代码生成器、编译时反射

C# 的编译期反射终于来啦!缓存

前言

今天 .NET 官方博客宣布 C# 9 Source Generators 第一个预览版发布,这是一个用户已经喊了快 5 年特性,今天终于发布了。安全

简介

Source Generators 顾名思义代码生成器,它容许开发者在代码编译过程当中获取查看用户代码而且生成新的 C# 代码参与编译过程,而且能够很好的与代码分析器集成提供 Intellisense、调试信息和报错信息,能够用它来作代码生成,所以也至关因而一个增强版本的编译时反射。框架

使用 Source Generators,能够作到这些事情:ide

  • 获取一个 Compilation 对象,这个对象表示了全部正在编译的用户代码,你能够从中获取 AST 和语义模型等信息
  • 能够向 Compilation 对象中插入新的代码,让编译器连同已有的用户代码一块儿编译

Source Generators 做为编译过程当中的一个阶段执行:函数

编译运行 -> [分析源代码 -> 生成新代码] -> 将生成的新代码添加入编译过程 -> 编译继续。工具

上述流程中,中括号包括的内容即为 Source Generators 所参与的阶段和能作到的事情。优化

做用

.NET 明明具有运行时反射和动态 IL 织入功能,那这个 Source Generators 有什么用呢?ui

编译时反射 - 0 运行时开销

拿 ASP.NET Core 举例,启动一个 ASP.NET Core 应用时,首先会经过运行时反射来发现 Controllers、Services 等的类型定义,而后在请求管道中须要经过运行时反射获取其构造函数信息以便于进行依赖注入。然而运行时反射开销很大,即便缓存了类型签名,对于刚刚启动后的应用也无任何帮助做用,并且不利于作 AOT 编译。this

Source Generators 将可让 ASP.NET Core 全部的类型发现、依赖注入等在编译时就所有完成并编译到最终的程序集当中,最终作到 0 运行时反射使用,不只利于 AOT 编译,并且运行时 0 开销。spa

除了上述做用以外,gRPC 等也能够利用此功能在编译时织入代码参与编译,不须要再利用任何的 MSBuild Task 作代码生成啦!

另外,甚至还能够读取 XML、JSON 直接生成 C# 代码参与编译,DTO 编写全自动化都是没问题的。

AOT 编译

Source Generators 的另外一个做用是能够帮助消除 AOT 编译优化的主要障碍。

许多框架和库都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它们在运行时从用户代码中发现类型。这些很是不利于 AOT 编译优化,由于为了使反射可以正常工做,必须将大量额外甚至可能不须要的类型元数据编译到最终的原生映像当中。

有了 Source Generators 以后,只须要作编译时代码生成即可以免大部分的运行时反射的使用,让 AOT 编译优化工具可以更好的运行。

例子

INotifyPropertyChanged

写过 WPF 或 UWP 的都知道,在 ViewModel 中为了使属性变动可被发现,须要实现 INotifyPropertyChanged 接口,而且在每个须要的属性的 setter 处触发属性更改事件:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
        }
    }
}

当属性多了以后将会很是繁琐,先前 C# 引入了 CallerMemberName 用于简化属性较多时候的状况:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

即,用 CallerMemberName 指示参数,在编译时自动填充调用方的成员名称。

可是仍是不方便。

现在有了 Source Generators,咱们能够在编译时生成代码作到这一点了。

为了实现 Source Generators,咱们须要写个实现了 ISourceGenerator 而且标注了 Generator 的类型。

完整的 Source Generators 代码以下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MySourceGenerator
{
    [Generator]
    public class AutoNotifyGenerator : ISourceGenerator
    {
        private const string attributeText = @"
using System;
namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}
";

        public void Initialize(InitializationContext context)
        {
            // 注册一个语法接收器,会在每次生成时被建立
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        public void Execute(SourceGeneratorContext context)
        {
            // 添加 Attrbite 文本
            context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));

            // 获取先前的语法接收器 
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;

            // 建立处目标名称的属性
            CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));

            // 获取新绑定的 Attribute,并获取INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

            // 遍历字段,只保留有 AutoNotify 标注的字段
            ListfieldSymbols = new List();
            foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
            {
                SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
                {
                    // 获取字段符号信息,若是有 AutoNotify 标注则保存
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }

            // 按 class 对字段进行分组,并生成代码
            foreach (IGroupinggroup in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
               context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
            }
        }

        private string ProcessClass(INamedTypeSymbol classSymbol, Listfields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
            {
                // TODO: 必须在顶层,产生诊断信息
                return null;
            }

            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

            // 开始构建要生成的代码
            StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");

            // 若是类型尚未实现 INotifyPropertyChanged 则添加实现
            if (!classSymbol.Interfaces.Contains(notifySymbol))
            {
                source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
            }

            // 生成属性
            foreach (IFieldSymbol fieldSymbol in fields)
            {
                ProcessField(source, fieldSymbol, attributeSymbol);
            }

            source.Append("} }");
            return source.ToString();
        }

        private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
        {
            // 获取字段名称
            string fieldName = fieldSymbol.Name;
            ITypeSymbol fieldType = fieldSymbol.Type;

            // 获取 AutoNotify Attribute 和相关的数据
            AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
            TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;

            string propertyName = chooseName(fieldName, overridenNameOpt);
            if (propertyName.Length == 0 || propertyName == fieldName)
            {
                //TODO: 没法处理,产生诊断信息
                return;
            }

            source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}
    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}
");

            string chooseName(string fieldName, TypedConstant overridenNameOpt)
            {
                if (!overridenNameOpt.IsNull)
                {
                    return overridenNameOpt.Value.ToString();
                }

                fieldName = fieldName.TrimStart('_');
                if (fieldName.Length == 0)
                    return string.Empty;

                if (fieldName.Length == 1)
                    return fieldName.ToUpper();

                return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
            }

        }

        // 语法接收器,将在每次生成代码时被按需建立
        class SyntaxReceiver : ISyntaxReceiver
        {
            public ListCandidateFields { get; } = new List();

            // 编译中在访问每一个语法节点时被调用,咱们能够检查节点并保存任何对生成有用的信息
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // 将具备至少一个 Attribute 的任何字段做为候选
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

有了上述代码生成器以后,之后咱们只须要这样写 ViewModel 就会自动生成通知接口的事件触发调用:

public partial class MyViewModel
{
    [AutoNotify]
    private string _text = "private field text";

    [AutoNotify(PropertyName = "Count")]
    private int _amount = 5;
}

上述代码将会在编译时自动生成如下代码参与编译:

public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public string Text
    {
        get 
        {
            return this._text;
        }
        set
        {
            this._text = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Text)));
        }
    }

    public int Count
    {
        get 
        {
            return this._amount;
        }
        set
        {
            this._amount = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Count)));
        }
    }
}

很是方便!

使用时,将 Source Generators 部分做为一个独立的 .NET Standard 2.0 程序集(暂时不支持 2.1),用如下方式引入到你的项目便可:


注意须要最新的 .NET 5 preview(写文章时还在 artifacts 里没正式 release),并指定语言版本为 preview:

preview

另外,Source Generators 须要引入两个 nuget 包:


限制

Source Generators 仅能用于访问和生成代码,可是不能修改已有代码,这有必定缘由是出于安全考量。

文档

Source Generators 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档:

设计文档

使用文档

后记

目前 Source Generators 仍处于很是早期的预览阶段,API 后期还可能会有很大的改动,所以现阶段不要用于生产。

另外,关于与 IDE 的集成、诊断信息、断点调试信息等的开发也在进行中,请期待后续的 preview 版本吧。

相关文章
相关标签/搜索