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

简介:

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

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

简介#
Source Generators 顾名思义代码生成器,它允许开发者在代码编译过程中获取查看用户代码并且生成新的 C# 代码参与编译过程,并且可以很好的与代码分析器集成提供 Intellisense、调试信息和报错信息,可以用它来做代码生成,因此也相当于是一个加强版本的编译时反射。

使用 Source Generators,可以做到这些事情:

获取一个 Compilation 对象,这个对象表示了所有正在编译的用户代码,你可以从中获取 AST 和语义模型等信息
可以向 Compilation 对象中插入新的代码,让编译器连同已有的用户代码一起编译
Source Generators 作为编译过程中的一个阶段执行:

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

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

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

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

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

除了上述作用之外,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 处触发属性更改事件:

Copy
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 用于简化属性较多时候的情况:

Copy
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 代码如下:

Copy
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 标注的字段
        List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
        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 (IGrouping<INamedTypeSymbol, IFieldSymbol> group 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, List<IFieldSymbol> fields, 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 List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

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

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

Copy
public partial class MyViewModel
{

[AutoNotify]
private string _text = "private field text";

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

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

Copy
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),用以下方式引入到你的项目即可:

Copy





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

Copy

preview

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

Copy




限制#
Source Generators 仅能用于访问和生成代码,但是不能修改已有代码,这有一定原因是出于安全考量。

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

设计文档

使用文档

后记#
目前 Source Generators 仍处于非常早期的预览阶段,API 后期还可能会有很大的改动,因此现阶段不要用于生产。

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

作者: hez2010

出处:https://www.cnblogs.com/hez2010/p/12810993.html

相关文章
|
6月前
|
C#
C#学习相关系列之数据类型类的三大特性(二)
C#学习相关系列之数据类型类的三大特性(二)
|
6月前
|
编译器 C# 开发者
C# 11.0中的新特性:覆盖默认接口方法
C# 11.0进一步增强了接口的灵活性,引入了覆盖默认接口方法的能力。这一新特性允许类在实现接口时,不仅可以提供接口中未实现的方法的具体实现,还可以覆盖接口中定义的默认方法实现。本文将详细介绍C# 11.0中接口默认方法覆盖的工作原理、使用场景及其对现有代码的影响,帮助开发者更好地理解和应用这一新功能。
|
6月前
|
C# 开发者
C# 10.0引入常量插值字符串:编译时确定性的新篇章
【1月更文挑战第22天】在C# 10.0中,微软为开发者带来了一项引人注目的新特性——常量插值字符串。这一功能允许在编译时处理和计算字符串插值表达式,从而得到可以在编译时确定的常量字符串。本文将深入探讨C# 10.0中常量插值字符串的概念、工作原理、使用场景及其对现有字符串处理方式的改进,旨在帮助读者更好地理解和应用这一强大的新特性。
|
6月前
|
编译器 C# 开发者
C# 9.0中的顶级语句:简化程序入口的新特性
【1月更文挑战第13天】本文介绍了C# 9.0中引入的顶级语句(Top-level statements)特性,该特性允许开发者在不使用传统的类和方法结构的情况下编写简洁的程序入口代码。文章详细阐述了顶级语句的语法、使用场景以及与传统程序结构的区别,并通过示例代码展示了其在实际应用中的便捷性。
|
28天前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
|
2月前
|
编译器 C# Android开发
震惊!Uno Platform 与 C# 最新特性的完美融合,你不可不知的跨平台开发秘籍!
Uno Platform 是一个强大的跨平台应用开发框架,支持 Windows、macOS、iOS、Android 和 WebAssembly,采用 C# 和 XAML 进行编程。C# 作为其核心语言,持续推出新特性,如可空引用类型、异步流、记录类型和顶级语句等,极大地提升了开发效率。要在 Uno Platform 中使用最新 C# 特性,需确保开发环境支持相应版本,并正确配置编译器选项。通过示例展示了如何在 Uno Platform 中应用可空引用类型、异步流、记录类型及顶级语句等功能,帮助开发者更好地构建高效、优质的跨平台应用。
176 59
|
19天前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
28 1
|
3月前
|
开发框架 .NET 编译器
总结一下 C# 如何自定义特性 Attribute 并进行应用
总结一下 C# 如何自定义特性 Attribute 并进行应用
|
3月前
|
编译器 C# Windows
C#基础:手动编译一个.cs源代码文件并生成.exe可执行文件
通过上述步骤,应该能够高效准确地编译C#源代码并生成相应的可执行文件。此外,这一过程强调了对命令行编译器的理解,这在调试和自动化编译流程中是非常重要的。
225 2
|
3月前
|
C# 索引
C#各大版本特性
C#各大版本特性
75 0