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
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
C#多线程(16):手把手教你撸一个工作流
C#多线程(16):手把手教你撸一个工作流 目录前言节点ThenParallelScheduleDelay试用一下顺序节点并行任务编写工作流接口构建器工作流构建器依赖注入实现工作流解析前言前面学习了很多多线程和任务的基础知识,这里要来实践一下啦。通过本篇教程,你可以写出一个简单的工作流引擎。 本篇教程内容完成是基于任务的,只需要看过笔者的三篇关于异步的文章,掌握 C# 基础,即可轻松完成。 C#多线程(13):任务基础①C#多线程(14):任务基础②C#多线程(15):任务基础③由于本篇文章编写的工作流程序,主要使用任务,有些逻辑过程会比较难理解,多测试一下就好。代码主要还是 C# 基础,为什么说简单? 不包含 async 、await几乎不含包含多线程(有个读写锁)不包含表达式树几乎不含反射(有个小地方需要反射一下,但是非常简单)没有复杂的算法因为是基于任务(Task)的,所以可以轻松设计组合流程,组成复杂的工作流。 由于只是讲述基础,所以不会包含很多种流程控制,这里只实现一些简单的。 先说明,别用到业务上。。。这个工作流非常简单,就几个功能,这个工作流是基于笔者的多线程系列文章的知...
- 下一篇
如何在 Ubuntu 20.04 上安装 Go
本文最先发布在:https://www.itcoder.tech/posts/how-to-install-go-on-ubuntu-20-04/ Go,通常被称为 golang,它是一门由 Google 创建的现代化的开源编程语言,它允许你构建实时并且高效的应用。 很多流行的应用程序,例如 Kubernetes,Docker,Prometheus 和 Terraform,都是使用 Go 来编写的。 这篇教程讲解如何在 Ubuntu 20.04 上下载和安装 Go。 一、在 Ubuntu 20.04 上安装 Go 完成下面的步骤,在 Ubuntu 20.04 上安装 Go 1.1 下载 Go 压缩包 在写这篇文章的时候,Go 的最新版为 1.14.2。在我们下载安装包时,请浏览Go 官方下载页面,并且检查一下是否有新的版本可用。 以 root 或者其他 sudo 用户身份运行下面的命令,下载并且解压 Go 二进制文件到/usr/local目录: wget -c https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz -O - | sud...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS8编译安装MySQL8.0.19
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS关闭SELinux安全模块
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7