首页 > 编程语言 >[C#]SourceGenerator实战: 对任意对象使用await吧!!!

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

时间:2022-10-20 22:59:10浏览次数:74  
标签:C# await value GetAwaiter 类型 SourceGenerator public

[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 的拓展方法类模板:

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 下,可以在使用的时候不需要额外的命名空间引用;
  • 由于我们已经有了需要返回的结果值,所以 AwaiterIsCompleted 始终为 trueGetResult 直接返回结果即可;

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

  1. 先建立一个 IncrementalGenerator
    [Generator(LanguageNames.CSharp)]
    public class GetAwaiterIncrementalGenerator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
        }
    }
    
  2. 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);   //使用默认的符号比较器进行比较
    
  3. 直接使用表达式语法不太方便处理,我们实现表达式语法到类型符号的转换方法 TransformAwaitExpressionSyntax
    private 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 方法的话,每次只会处理一个类型符号,我们无法去重
  • 调用 symbolProviderCollect 方法,可以将前面步骤筛选出的所有类型符号作为一个集合进行处理

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

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 任何对象了,效果大概如下:

标签:C#,await,value,GetAwaiter,类型,SourceGenerator,public
From: https://www.cnblogs.com/internalnet/p/16811643.html

相关文章

  • Django 跨域CORS
    解决后端对跨域访问的支持。安装pipinstalldjango-cors-headers注册应用INSTALLED_APPS=[...,corsheaders,...]中间件设置MIDDLEWARE=[......
  • Tomcat&Servlet笔记
    今日内容1.web相关概念回顾2.web服务器软件:Tomcat3.Servlet入门学习web相关概念回顾1.软件架构 1.C/S:客户端/服务器端 2.B/S:浏览器/服务器端2.资源分类 ......
  • 拉格朗日反演推导扩展Cayley公式
    萌新初学拉格朗日反演,这个看起来很对所以应该是对的吧?把\(n\)个点连成有\(k\)棵树的森林,并且要求\(1,2,\cdots,k\)这\(k\)个点两两不在同一棵树的方案数。首先......
  • 在Native C++中调用C#代码
     在关于C++与C#互操作的大多中文文章中,介绍都是在C#中如何使用C++的功能,本文将为大家介绍在C++中如何调用C#的功能。首先,简单介绍一下C#如何使用C++的功能,以作者所......
  • C++ 中 const 关键字的作用总结
    const的含义相信大部分程序员都对const不陌生,英文翻译中作为形容词意思为恒定的,不变的,作为名词翻译为常量,恒量,其实,这在很大程度上已经说明了这个关键字的含义。接下来,让......
  • Spring Batch 中的 chunk
    我们都知道SpringBatch有2种任务方式。主要是在Step阶段,在Step阶段,我们可以执行一个Tasklet,我们也可以按照Chunk来执行。主要区别如果使用Tasklet的话,我们可......
  • Spring Batch 中的 chunk
    我们都知道SpringBatch有2种任务方式。主要是在Step阶段,在Step阶段,我们可以执行一个Tasklet,我们也可以按照Chunk来执行。主要区别如果使用Tasklet的话,我......
  • LCA
    #include<bits/stdc++.h>usingnamespacestd;#defineintlonglong#defineullunsignedlonglong#defineendl"\n"#definesfscanf#definepfprintf#define......
  • 嵌入式-C语言基础:理解形参和实参的区别
    #include<stdio.h>//实参:函数原型中声明函数后面带的参数inttest(intx)//函数原型{//函数体printf("test里面的x地址=%p",&x);returnx;}//变量......
  • CF916E 解题报告
    被这道题搞了一个晚上,还好搞出来了qwq令人耳目一新的阅读体验题目简述翻译已经很简单了。前置知识DFS序,LCA,线段树,不需要标签中的树剖!DFS序更新信息及判断祖先如果你......