2026 年来讲这个特性可能有点火星了,不过 C# Interceptors 确实给了我一些惊喜,故想写篇文章介绍一下。
Interceptors 在 .NET 8 上面是实验性功能,.NET 9.0.2xx 之后成为稳定性功能。
在 .NET 10 LTS 之后的版本开始流行使用这个特性。
一上来就看 Interceptors 是比较难以理解的,它通过 Analyzer hook 进编译管线,这里就先从 Analyzer 讲起。
为什么要用它?
我们使用编译器魔法,本质上就是元编程,可以将反射开销全部优化掉,速度变快,也能 AOT (Ahead of time) 友好,AOT 是近年 C# 快速发展的一个特性。
Roslyn Analyzer
理解 C# 编译器管线之前,需要知道 Roslyn 起到一个什么样的作用。
类似于 Python ast,它们起到的作用都是 解析 (Parse) 和 分析 (Analysis) 语法树。 Parse 的部分大差不差,在分析部分 Python ast 仅仅提供了一个简易的 NodeVisitor,而 Roslyn 提供了一整套的 Semantic Model,无需像 Python 那样写复杂的代码分析。
Semantic Model 就是一个获取类型推断信息的语义模型,最直观的就是 IDE 的 Go to Definition。
Python 写 Semantic Model 的痛苦程度看看 mypy 就知道了(以及我写的 nb-autodoc)。
Roslyn 相当于是微软提供的轮椅,它包含了一整套分析静态代码产生完整类型推断的工具链,这些信息都是和 Ast 相关联的,Linter 可以从中拿到类型推断,产生分析结果,最终反馈在源码的具体每个字符上。
现代,将各种编译器诊断接入到 IDE 用 LSP (Language Server Protocol) 已然是标准做法,而各种不同编程语言如何编写 Linter 则产生了非常多的方法。
- Go (gopls)、C++ (clangd) 等用的是官方编译器 API。
- Python (pyright) 则比较别具一格地用 Typescript 来解析 Python 的源码。
- TypeScript (tsc/eslint) 则是类型检查 -> 类型擦除 -> JS Linter。
C# 是其中最灵活的一种插件式系统,它定义了一种叫做 Analyzer 的项目类型,通过将 Analyzer 定义在待分析项目依赖里面,编译器进程去读取并加载,无需用户去引入第三方工具、兼容 IDE 什么的。
从 IDE 的角度来看执行流程:
|
|
需要注意的是 Roslyn 和 Analyzer 本身都是一个包,它们的宿主是编译器。
为了兼容各个版本 dotnet SDK 或者甚至不同编译器的 TargetFrameworks 支持列表,Analyzer 项目版本必须要和各种编译器进程兼容,即公认 netstandard2.0。
Roslyn Analyzer 相当于是一个用 C# 实现的 C# 分析器罢了,本质上也可以用其他语言来实现,例如 F#,前提是兼容
netstandard2.0标准的 dll 程序集。
回顾一下 C# 进行一次编译的时候,输入是多个源代码文件 + .csproj package reference + project reference + OutputType 等多个编译选项,输出就是 <assemblyName>.dll。
加入代码诊断 Diagnostics Analyzer 的编译流程大概上就是这样:
|
|
Source Generator
Source Generator (SG) 大约在 2020 年推出,一种在不修改源码基础上新增代码的方式,在最终编译的时候才将这些新文件一起编译进去。
SG 一般会配合 attribute / partial 这两个 C# 特性一起用,例如先用声明式写一段代码:
|
|
通过在项目定义一个 SG Analyzer(这个具体怎么写就不展开了)并开启 <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 调试选项,就会在 obj/Debug/net10.0/generated 下面看到一个新文件:
|
|
SG 的特点是在完全不改变用户代码语义(包括类型检查)的情况下,用 partial / extension method 等方法去拓展源码。
SG 也是 Analyzer,上面说的诊断 Analyzer 继承 DiagnosticAnalyzer 基类,而 SG 基于 IIncrementalGenerator 接口实现。
加入了 Source Generator 的编译流程是这样的:
|
|
Interceptors
Interceptors 大约在 2025 年底成为稳定特性,可以拦截函数调用并替换为另一个调用。
理解了 Analyzer 之后,再来阅读 Interceptors 官方文档 就很清晰易懂了。
Interceptors 本质和 SG 一样,都是基于 IIncrementalGenerator 接口实现,都是 Generate New Files。
Interceptors 特性就是不需要通过魔改代码,实现拦截并替换项目内任何地方的 Invocation(函数调用)。
原理:Interceptors 和 SG 同样是产生新文件,区别在于 Interceptors 扫描代码时可以根据 Invocation 创建一个拦截点,将其重定向到新文件里的新函数,交给 Compiler 去处理拦截点。
编译流程在 Compile 之后多了一个 Interceptors Replace 的步骤,指导 Compiler 去接管 指定位置 代码的函数调用:
|
|
文档介绍的 GetInterceptableLocation(this SemanticModel, InvocationExpressionSyntax, CancellationToken) 便是用于生成位置编码的函数。
整体看下来还是比较简单的,做 ast 本身就有很多 polyfill 这里就不讲了,实现的时候就知道了。
Iyume.JsonPath 是我根据 Interceptors 特性 vibe coding 出来的,可以在编译时就确定 JsonPath 的路径生成静态代码,感兴趣的也可以看看。 (有些细节还没空去调整,比如生成的 Location 与函数如何复用)