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 等的b2b供应网类型定义,然后在请求管道中需要通过运行时反射获取其构造函数信息以便于进行依赖注入。然而运行时反射开销很大,即使缓存了类型签名,对于刚刚启动后的应用也无任何帮助作用,而且不利于做 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 处除法属性更改事件:
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 标注的字段 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 就会自动生成通知接口的事件触发调用:
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),用以下方式引入到你的项目即可:
<ItemGroup> <Analyzer Include="..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" /> </ItemGroup>注意需要 .NET 5 preview 3 或以上版本,并指定语言版本为 preview :
<PropertyGroup> <LangVersion>preview</LangVersion> </PropertyGroup>另外,Source Generators 需要引入两个 nuget 包:
<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" /> </ItemGroup>限制
Source Generators 仅能用于访问和生成代码,但是不能修改已有代码,这有一定原因是出于安全考量。
文档
Source Generators 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档:
设计文档
使用文档
后记
目前 Source Generators 仍处于非常早期的预览阶段,API 后期还可能会有很大的改动,因此现阶段不要用于生产。
另外,关于与 IDE 的集成、诊断信息、断点调试信息等的开发也在进行中,请期待后续的 preview 版本吧。
相关文章
猛禽GTX1070的性能及特点剖析(一款强劲的显卡在游戏领域的王者归来)
摘要:随着电子竞技的兴起和游戏画面的逐渐提升,对于高性能显卡的需求也越来越迫切。作为NVIDIA旗下的一款旗舰级显卡,猛禽GTX1070凭借其强大的性能和卓越的特点,在游戏领域中引起了广...2025-11-05中航打印机的性能与特点(打印速度快、打印质量高、使用便捷、适用广泛)
摘要:随着科技的不断进步,打印机已经成为办公和个人生活中必不可少的设备。其中,中航打印机以其出色的性能和优秀的特点赢得了广大用户的青睐。一:高速打印——提升工作效率中航打印机...2025-11-05小米手机4X电池表现如何?(探究小米手机4X电池续航能力和充电速度,带你了解真实体验!)
摘要:在现代社会中,手机已经成为我们生活中必不可少的工具之一。而对于手机来说,电池的续航能力和充电速度是用户最为关心的问题之一。本文将重点关注小米手机4X的电池表现,探究其续航能力和充电...2025-11-05《使用MSDN原版Win7进行安装教程》(详细指导,让您轻松安装Windows7)
摘要:在如今的数字时代,Windows7依然是广大用户中非常受欢迎的操作系统之一。本篇文章将为您提供一份详细的教程,教您如何使用MSDN原版Win7进行安装,帮助您在安装过程中更加顺利和...2025-11-05Lenovo100S评测(一款轻薄本打破常规,满足日常办公需求)
摘要:作为一名经常需要外出办公的人士,一台轻便且性能出众的笔记本电脑是非常重要的。今天我们将为大家带来Lenovo100S的评测,探讨其在性能和便携性方面的表现。1.轻薄本的...2025-11-05雷蛇诺斯魔舰键盘助力DOTA,打造绝佳游戏体验(高性能键盘助您在DOTA中游刃有余)
摘要:作为一款热门的电竞游戏,DOTA对玩家的操作和反应速度要求较高。而雷蛇诺斯魔舰键盘作为一款专为电竞而设计的高性能游戏键盘,能够为玩家提供出色的游戏体验。本文将详细介绍雷蛇诺斯魔舰键...2025-11-05


最新评论