[C#]SourceGenerator实战: 对任意对象使用await吧!!!

前言

本文记录一次简单的 SourceGenerator 实战,最终实现可以在代码中 await 任意类型对象,仅供娱乐,请勿在生产环境中使用!!!

关键技术:

  • SourceGenerator

  • Await anything

    • C#中的 async/await 最终由编译器编译为状态机,其核心逻辑在于 await 对象需要实现符合要求的 GetAwaiter 方法,这个方法可以是 拓展方法
    • 参见官方博客 await anything;

那么要实现对任何对象的 await 我们的思路大概如下:

  1. 找到所有的 await 语法
  2. 检查 await 的对象是否有 GetAwaiter 方法
  3. 为没有 GetAwaiter 方法的对象生成 GetAwaiter 拓展方法

得益于 SourceGenerator 丰富的分析API,我们可以很容易的办到这件事


实现源生成器

GetAwaiter拓展方法模板

我们先来实现一个可以让 TargetType 支持 await 的拓展方法类模板:

  1. using System.Runtime.CompilerServices;
  2. namespace System.Threading.Tasks
  3. {
  4. public static class GetAwaiterExtension_TargetTypeName
  5. {
  6. public static TaskAwaiterFor_TargetTypeName GetAwaiter(this TargetType value)
  7. {
  8. return new TaskAwaiterFor_TargetTypeName(value);
  9. }
  10. public readonly struct TaskAwaiterFor_TargetTypeName : ICriticalNotifyCompletion, INotifyCompletion
  11. {
  12. private readonly TargetType _value;
  13. public bool IsCompleted { get; } = true;
  14. public TaskAwaiterFor_TargetTypeName(TargetType value)
  15. {
  16. _value = value;
  17. }
  18. public TargetType GetResult()
  19. {
  20. return _value;
  21. }
  22. public void OnCompleted(Action continuation)
  23. {
  24. continuation();
  25. }
  26. public void UnsafeOnCompleted(Action continuation)
  27. {
  28. continuation();
  29. }
  30. }
  31. }
  32. }
  • 将类型放在命名空间 System.Threading.Tasks 下,可以在使用的时候不需要额外的命名空间引用;
  • 由于我们已经有了需要返回的结果值,所以 AwaiterIsCompleted 始终为 trueGetResult 直接返回结果即可;

分析所有 await 语法,并筛选出需要为其生成 GetAwaiter 方法的类型

  1. 先建立一个 IncrementalGenerator
    1. [Generator(LanguageNames.CSharp)]
    2. public class GetAwaiterIncrementalGenerator : IIncrementalGenerator
    3. {
    4. public void Initialize(IncrementalGeneratorInitializationContext context)
    5. {
    6. }
    7. }
  2. Initialize 方法中筛选目标类型
    1. /// 使用语法提供器筛选出所有的 `await` 语法,并获取其类型
    2. var symbolProvider = context.SyntaxProvider.CreateSyntaxProvider((node, _) => node is AwaitExpressionSyntax //直接判断节点是否为 `AwaitExpressionSyntax` 即可筛选出所有 await 表达式
    3. , TransformAwaitExpressionSyntax) //从 await 表达式中解析出其尚不支持 await 的对象类型符号
    4. .Where(m => m is not null) //筛选掉无效的项
    5. .WithComparer(SymbolEqualityComparer.Default); //使用默认的符号比较器进行比较
  3. 直接使用表达式语法不太方便处理,我们实现表达式语法到类型符号的转换方法 TransformAwaitExpressionSyntax
    1. private static ITypeSymbol? TransformAwaitExpressionSyntax(GeneratorSyntaxContext generatorSyntaxContext, CancellationToken cancellationToken)
    2. {
    3. //经过筛选,到达此处的节点一定是 AwaitExpressionSyntax
    4. var awaitExpressionSyntax = (AwaitExpressionSyntax)generatorSyntaxContext.Node;
    5. //如果 await 表达式语法的 await 对象仍然是 AwaitExpressionSyntax ,那么跳过此条记录
    6. //类似 "await await await 1;" 我们直接忽略前两个 await 表达式
    7. if (awaitExpressionSyntax.Expression is AwaitExpressionSyntax)
    8. {
    9. return null;
    10. }
    11. //使用 `SemanticModel` 可以分析出更具体的符号信息,比如类型,方法等
    12. //直接使用其提供的 `GetAwaitExpressionInfo` 可以从表达式语法获取 await 的详细信息
    13. var awaitExpressionInfo = generatorSyntaxContext.SemanticModel.GetAwaitExpressionInfo(awaitExpressionSyntax);
    14. //判断分析结果中此表达式是否包含 `GetAwaiter` 方法,如果不包含,那么我们需要为其生成
    15. if (awaitExpressionInfo.GetAwaiterMethod is null)
    16. {
    17. //`SemanticModel` 的 GetTypeInfo 方法可以获取一个表达式的类型符号信息
    18. //返回 await 对象的类型符号
    19. return generatorSyntaxContext.SemanticModel.GetTypeInfo(awaitExpressionSyntax.Expression).Type;
    20. }
    21. return null;
    22. }

为所有目标类型生成 GetAwaiter 拓展方法

由于只需要为相同类型生成一次 GetAwaiter 方法,所以我们需要将类型符号去重之后进行生成

  • 直接将上面的 symbolProvider 传递给 RegisterSourceOutput 方法的话,每次只会处理一个类型符号,我们无法去重
  • 调用 symbolProviderCollect 方法,可以将前面步骤筛选出的所有类型符号作为一个集合进行处理

所以注册源码生成器可以这样写:

  1. context.RegisterSourceOutput(symbolProvider.Collect(), //将筛选的结果作为整体传递
  2. (ctx, input) =>
  3. {
  4. //遍历去重后的类型符号
  5. foreach (var item in input.Distinct(SymbolEqualityComparer.Default))
  6. {
  7. //为每个去重后的类型生成 `GetAwaiter` 拓展方法
  8. }
  9. });

接下来使用之前写的拓展方法模板生成每个类型的 GetAwaiter 拓展方法即可:

  1. //获取类型符号的完整访问类型名
  2. var fullyClassName = item!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  3. //获取不包含无效符号的类名
  4. var className = NormalizeClassName(fullyClassName);
  5. //替换模板中的类型占位符为当前处理的目标类型
  6. var code = templateCode.Replace("TargetTypeName", className)
  7. .Replace("TargetType", fullyClassName);
  8. //如果目标类型不是公开类型,那么拓展方法也应该不公开
  9. if (item.DeclaredAccessibility != Accessibility.Public)
  10. {
  11. code = code.Replace("public static class", "internal static class");
  12. }
  13. //将生成的代码添加到编译中
  14. ctx.AddSource($"GetAwaiterFor_{className}.g.cs", code);
  1. //将类型名称中不能作为类名的符号替换为_
  2. private static string NormalizeClassName(string value)
  3. {
  4. return value.Replace('.', '_')
  5. .Replace('<', '_')
  6. .Replace('>', '_')
  7. .Replace(' ', '_')
  8. .Replace(',', '_')
  9. .Replace(':', '_');
  10. }

到这里我们就实现了所有的功能点,新建项目并引用分析器就可以 await 任何对象了,效果大概如下:

[C#]SourceGenerator实战: 对任意对象使用await吧!!!的更多相关文章

  1. Progress.js – 为页面上的任意对象创建进度条效果

    Progress.js 是一个 JavaScript 和 CSS3 的库,它帮助开发人员为网页上的每个对象创建和管理进度条效果.你可以设计自己的模板,进度条或者干脆定制. 您可以使用 Progress ...

  2. Js中找任意对象的原型方法及改造原型

    Java中有运行时类型识别,js可以很方便的模仿这个特性,因为所有js对象都有一个属性constructor(构造器),表示这个对象的构造方法,原型与构造方法同名,所以可以通过这儿知道任意对象的原型名 ...

  3. (一一五)利用NSKeyedArchiver实现任意对象转为二进制

    [应用背景] 在数据库中存储数据时,如果对象过于复杂,又不必要创建复杂的表,可以直接把整个对象转化为二进制存入数据库字段,然后取出后再还原即可. [实现方法] 在PHP中,使用序列化和反序列化可以实现 ...

  4. Java多线程6:Synchronized锁代码块(this和任意对象)

    一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...

  5. synchronized将任意对象作为对象监视器

    多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的.这说明synchronized同 ...

  6. IOS-将任意对象存进数据库

    model // // HMShop.h // 将任意对象存进数据库 // // Created by apple on 14/11/20. // Copyright (c) 2014年 heima. ...

  7. 直接获取任意对象的 $('.xx').css('x') 值都是0

    <!-- 任意对象,直接获取他们的 x , y 都是为0: $('#xxx').css('x','y'); --> <!DOCTYPE html> <html lang= ...

  8. C# 将任意对象快速转换为Json

    由于最近项目里面Model类特别多,而我需要编写所有数据交互的接口,传输的格式是json,以前都是通过循环List<T>中的对象向StringBuilder对象中Apped字符串生成jso ...

  9. 【WePY小程序框架实战四】-使用async&await异步请求数据

    [WePY小程序框架实战一]-创建项目 [WePY小程序框架实战二]-页面结构 [WePY小程序框架实战三]-组件传值 async await 是对promise的近一步优化,既解决了promise链 ...

随机推荐

  1. Postgres常用SQL

  2. 【web自动化测试】playwright安装失败怎么办

    在安装中,如果没有输入 playwright install, 则不会安装浏览器,运行 playwright codegen 时会报浏览器找不到的错误: "chromium" br ...

  3. Eplan创建符号详细解说

    如何创建新的符号库以及在项目里面导入这个新的符号库 这个就不详细的赘述了 网上很多教程 和我一样的萌新可以参考 https://wenku.baidu.com/view/18c16641e45c3b3 ...

  4. 《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(10)-Fiddler如何设置捕获Firefox浏览器的Https会话

    1.简介 经过上一篇对Fiddler的配置后,绝大多数的Https的会话,我们可以成功捕获抓取到,但是有些版本的Firefox浏览器仍然是捕获不到其的Https会话,需要我们更进一步的配置才能捕获到会 ...

  5. 活动报名|对话贡献者:DolphinScheduler x Pulsar 在线 Meetup

    各位 DolphinScheduler 和 Pulsar 社区的小伙伴们,Apache DolphinScheduler x Pulsar 在线 Meetup 来啦! 导语 大数据任务调度.消息流的订 ...

  6. 初次认识 Canvas

    画布的概念 Canvas(画布)可以用于动画.游戏画面.数据可视化.图片编辑以及实时视频处理等方面.画布在 HTML5 中是通过canvas标签来表现,通过 JavaScript 提供的画布 API, ...

  7. 【碳硫磷模拟赛】消失的+和* (树形DP)

    好久没做过这么恶心的DP题了 题面 题面很简单,有一个计算式,由+号.*号.括号和小于10的正整数组成,现在所有的+和*(由于属于违禁词而)都被-号给和谐掉了,现在要求所有可能的原计算式的结果之和. ...

  8. MQ系列4:NameServer 原理解析

    MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 MQ系列3:RocketMQ 架构分析 1 关于NameServer 上一节的 MQ系列3:RocketMQ 架构分析,我们大致介绍了 ...

  9. SpringMVC 04: SpringMVC中4种页面跳转方式

    转发和重定向的页面跳转方式 页面跳转方式,本质上只有2种方式:转发 + 重定向 但在SpringMVC的具体实现上,转发可以细分为:普通的页面转发 + 经由action方法的页面转发 重定向可以细分为 ...

  10. 搭建eBackup对接NFS服务

    环境准备 两个虚拟机需要是仅主机并且同一网段 先搭建一个eBackup环境虚拟机 搭建步骤可访问:(https://www.cnblogs.com/zhengyan6/p/16220774.html) ...