[C#]SourceGenerator实战: 对任意对象使用await吧!!!
[C#]SourceGenerator实战: 对任意对象使用await吧!!!
前言
本文记录一次简单的 SourceGenerator 实战,最终实现可以在代码中 await 任意类型对象,仅供娱乐,请勿在生产环境中使用!!!
关键技术:
SourceGenerator
- 在编译时动态生成代码的技术。官方文档:源生成器
- 关于
IncrementalGenerator的基本使用可以参考 dotnet 用 SourceGenerator 源代码生成技术实现中文编程语言 SourceGenerator除了提供附加文件进行代码生成,还有丰富的语法树、类型等分析APISourceGenerator只能拓展代码,不能替换代码
Await anything
- C#中的
async/await最终由编译器编译为状态机,其核心逻辑在于await对象需要实现符合要求的GetAwaiter方法,这个方法可以是拓展方法 - 参见官方博客 await anything;
- C#中的
那么要实现对任何对象的 await 我们的思路大概如下:
- 找到所有的
await语法 - 检查
await的对象是否有GetAwaiter方法 - 为没有
GetAwaiter方法的对象生成GetAwaiter拓展方法
得益于 SourceGenerator 丰富的分析API,我们可以很容易的办到这件事
实现源生成器
GetAwaiter拓展方法模板
我们先来实现一个可以让 TargetType 支持 await 的拓展方法类模板:
using System.Runtime.CompilerServices;
namespace System.Threading.Tasks
{
public static class GetAwaiterExtension_TargetTypeName
{
public static TaskAwaiterFor_TargetTypeName GetAwaiter(this TargetType value)
{
return new TaskAwaiterFor_TargetTypeName(value);
}
public readonly struct TaskAwaiterFor_TargetTypeName : ICriticalNotifyCompletion, INotifyCompletion
{
private readonly TargetType _value;
public bool IsCompleted { get; } = true;
public TaskAwaiterFor_TargetTypeName(TargetType value)
{
_value = value;
}
public TargetType GetResult()
{
return _value;
}
public void OnCompleted(Action continuation)
{
continuation();
}
public void UnsafeOnCompleted(Action continuation)
{
continuation();
}
}
}
}
- 将类型放在命名空间
System.Threading.Tasks下,可以在使用的时候不需要额外的命名空间引用; - 由于我们已经有了需要返回的结果值,所以
Awaiter的IsCompleted始终为true,GetResult直接返回结果即可;
分析所有 await 语法,并筛选出需要为其生成 GetAwaiter 方法的类型
- 先建立一个
IncrementalGenerator[Generator(LanguageNames.CSharp)]
public class GetAwaiterIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
- 在
Initialize方法中筛选目标类型/// 使用语法提供器筛选出所有的 `await` 语法,并获取其类型
var symbolProvider = context.SyntaxProvider.CreateSyntaxProvider((node, _) => node is AwaitExpressionSyntax //直接判断节点是否为 `AwaitExpressionSyntax` 即可筛选出所有 await 表达式
, TransformAwaitExpressionSyntax) //从 await 表达式中解析出其尚不支持 await 的对象类型符号
.Where(m => m is not null) //筛选掉无效的项
.WithComparer(SymbolEqualityComparer.Default); //使用默认的符号比较器进行比较
- 直接使用表达式语法不太方便处理,我们实现表达式语法到类型符号的转换方法
TransformAwaitExpressionSyntaxprivate static ITypeSymbol? TransformAwaitExpressionSyntax(GeneratorSyntaxContext generatorSyntaxContext, CancellationToken cancellationToken)
{
//经过筛选,到达此处的节点一定是 AwaitExpressionSyntax
var awaitExpressionSyntax = (AwaitExpressionSyntax)generatorSyntaxContext.Node; //如果 await 表达式语法的 await 对象仍然是 AwaitExpressionSyntax ,那么跳过此条记录
//类似 "await await await 1;" 我们直接忽略前两个 await 表达式
if (awaitExpressionSyntax.Expression is AwaitExpressionSyntax)
{
return null;
} //使用 `SemanticModel` 可以分析出更具体的符号信息,比如类型,方法等
//直接使用其提供的 `GetAwaitExpressionInfo` 可以从表达式语法获取 await 的详细信息
var awaitExpressionInfo = generatorSyntaxContext.SemanticModel.GetAwaitExpressionInfo(awaitExpressionSyntax); //判断分析结果中此表达式是否包含 `GetAwaiter` 方法,如果不包含,那么我们需要为其生成
if (awaitExpressionInfo.GetAwaiterMethod is null)
{
//`SemanticModel` 的 GetTypeInfo 方法可以获取一个表达式的类型符号信息
//返回 await 对象的类型符号
return generatorSyntaxContext.SemanticModel.GetTypeInfo(awaitExpressionSyntax.Expression).Type;
} return null;
}
为所有目标类型生成 GetAwaiter 拓展方法
由于只需要为相同类型生成一次 GetAwaiter 方法,所以我们需要将类型符号去重之后进行生成
- 直接将上面的
symbolProvider传递给RegisterSourceOutput方法的话,每次只会处理一个类型符号,我们无法去重 - 调用
symbolProvider的Collect方法,可以将前面步骤筛选出的所有类型符号作为一个集合进行处理
所以注册源码生成器可以这样写:
context.RegisterSourceOutput(symbolProvider.Collect(), //将筛选的结果作为整体传递
(ctx, input) =>
{
//遍历去重后的类型符号
foreach (var item in input.Distinct(SymbolEqualityComparer.Default))
{
//为每个去重后的类型生成 `GetAwaiter` 拓展方法
}
});
接下来使用之前写的拓展方法模板生成每个类型的 GetAwaiter 拓展方法即可:
//获取类型符号的完整访问类型名
var fullyClassName = item!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
//获取不包含无效符号的类名
var className = NormalizeClassName(fullyClassName);
//替换模板中的类型占位符为当前处理的目标类型
var code = templateCode.Replace("TargetTypeName", className)
.Replace("TargetType", fullyClassName);
//如果目标类型不是公开类型,那么拓展方法也应该不公开
if (item.DeclaredAccessibility != Accessibility.Public)
{
code = code.Replace("public static class", "internal static class");
}
//将生成的代码添加到编译中
ctx.AddSource($"GetAwaiterFor_{className}.g.cs", code);
//将类型名称中不能作为类名的符号替换为_
private static string NormalizeClassName(string value)
{
return value.Replace('.', '_')
.Replace('<', '_')
.Replace('>', '_')
.Replace(' ', '_')
.Replace(',', '_')
.Replace(':', '_');
}
到这里我们就实现了所有的功能点,新建项目并引用分析器就可以 await 任何对象了,效果大概如下:

- 代码 - AwaitAnyObject.zip
- 也可以直接安装 NuGet 包
AwaitAnyObject进行游玩
[C#]SourceGenerator实战: 对任意对象使用await吧!!!的更多相关文章
- Progress.js – 为页面上的任意对象创建进度条效果
Progress.js 是一个 JavaScript 和 CSS3 的库,它帮助开发人员为网页上的每个对象创建和管理进度条效果.你可以设计自己的模板,进度条或者干脆定制. 您可以使用 Progress ...
- Js中找任意对象的原型方法及改造原型
Java中有运行时类型识别,js可以很方便的模仿这个特性,因为所有js对象都有一个属性constructor(构造器),表示这个对象的构造方法,原型与构造方法同名,所以可以通过这儿知道任意对象的原型名 ...
- (一一五)利用NSKeyedArchiver实现任意对象转为二进制
[应用背景] 在数据库中存储数据时,如果对象过于复杂,又不必要创建复杂的表,可以直接把整个对象转化为二进制存入数据库字段,然后取出后再还原即可. [实现方法] 在PHP中,使用序列化和反序列化可以实现 ...
- Java多线程6:Synchronized锁代码块(this和任意对象)
一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...
- synchronized将任意对象作为对象监视器
多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的.这说明synchronized同 ...
- IOS-将任意对象存进数据库
model // // HMShop.h // 将任意对象存进数据库 // // Created by apple on 14/11/20. // Copyright (c) 2014年 heima. ...
- 直接获取任意对象的 $('.xx').css('x') 值都是0
<!-- 任意对象,直接获取他们的 x , y 都是为0: $('#xxx').css('x','y'); --> <!DOCTYPE html> <html lang= ...
- C# 将任意对象快速转换为Json
由于最近项目里面Model类特别多,而我需要编写所有数据交互的接口,传输的格式是json,以前都是通过循环List<T>中的对象向StringBuilder对象中Apped字符串生成jso ...
- 【WePY小程序框架实战四】-使用async&await异步请求数据
[WePY小程序框架实战一]-创建项目 [WePY小程序框架实战二]-页面结构 [WePY小程序框架实战三]-组件传值 async await 是对promise的近一步优化,既解决了promise链 ...
随机推荐
- Chapter 02 - Let's Get Started(C#篇)
详细解释,书上有哈.直接上代码和结果. Xcode下的自定义类 (通过new file-> cocoa class创建,保持和书中名字一样RandomController),自定义的fields ...
- 越折腾越好用的 3 款开源 APP
高中的时候我特别喜欢捣鼓手机,然后我一个哥们儿在我的强烈推荐下买了个 HTC Dream(G1) 手机. G1 作为谷歌的第一个亲儿子,它出厂搭载的是 Android 1.5 系统,但当时已经出到了 ...
- PerfView专题 (第二篇):如何寻找 C# 中的 Heap堆内存泄漏
一:背景 上一篇我们聊到了如何去找 热点函数,这一篇我们来看下当你的程序出现了 非托管内存泄漏 时如何去寻找可疑的代码源头,其实思路很简单,就是在 HeapAlloc 或者 VirtualAlloc ...
- Mysql 实现数据库读写分离
Amoeba+Mysql实现数据库读写分离 一.Amoeba 是什么 Amoeba(变形虫)项目,专注 分布式数据库 proxy 开发.座落与Client.DB Server(s)之间.对客户端透明. ...
- flutter系列之:widgets,构成flutter的基石
目录 简介 StatelessWidget和StatefulWidget StatelessWidget详解 StatefulWidget详解 总结 简介 flutter中所有的组件都是由widget ...
- python九周周末总结
python九周周末总结 UDP协议 udp协议的交互模式服务端不需要考虑客户端是否退出,你发多少那么他就会按照你发的东西直接去传输给客户端不存在黏包现象 服务端: import socket ser ...
- 贪吃蛇(C语言版)链表实现
贪吃蛇 gitee:贪吃蛇C语言版: Snake 蛇的结构 typedef struct Snake { int x; int y; struct Snake *next; }; 游戏开始欢迎界面 / ...
- 【java】学习路线9-非静态内部类、外部类
//内部类只能在其外部类当中使用//局部内部类:定义在方法里面//如果内部类和外部类有重名,就近原则在内部类中优先访问内部类.//如果想访问宿主类的同名成员,使用OuterClass.this.xxx ...
- 算法:KMP, str1字符串是否包含str2字符串
[普通解法]从左到右遍历str1的每一个字符,然后看如果 以当前字符作为第一个字符出发 是否匹配 str2字符串. [KMP算法] 1)生成一个nextArr数组,长度与str2字符串长度一样.i 的 ...
- 安装vm,在vm中安装windows10操作系统。
步骤:双击打开虚拟机文件 根据向导安装 下一步 然后等待安装 安装好了后点击许可证 ZF3R0-FHED2-M80TY-8QYGC-NPKYF YF390-0HF8P-M81RQ-2DXQE-M2U ...