Appearance
第 7 章:语法与语义
本章目标
- 彻底区分“语法”和“语义”
- 知道为什么源生成器通常既要看语法,也要看语义
- 理解
ClassDeclarationSyntax和INamedTypeSymbol的角色分工
先看现象
如果你只拿到一段 C# 文本,你最多能知道:
- 这里像一个类声明
- 这里像一个属性声明
- 这里写了一个特性
但你还不知道:
- 它真正属于哪个命名空间
- 它的属性类型是不是可空类型
- 它是不是泛型类
- 它到底是哪个真实类型,而不是只看名字长什么样
这些更深一层的信息,不属于“语法长相”,而属于“语义含义”。
再看代码
主案例里有一个很典型的转折:
csharp
if (context.TargetNode is not ClassDeclarationSyntax classDeclaration)
{
return null;
}
var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration, token) as INamedTypeSymbol;这里前后两步分别站在两个世界里:
第一步:语法层
csharp
ClassDeclarationSyntax这一步回答的问题是:
- 这是不是一个类声明
- 它在源代码里长什么样
这里还有一个很容易被忽略的点:
TargetNode的静态类型通常先是SyntaxNode- 你要先判断它是不是
ClassDeclarationSyntax - 之后才能安全地把它当成“类声明节点”继续处理
第二步:语义层
csharp
INamedTypeSymbol这一步回答的问题是:
- 这个类真正代表哪个类型
- 它属于哪个命名空间
- 它是不是泛型类
- 它有哪些成员和类型信息
SemanticModel 起到什么作用
SemanticModel 可以理解成一座桥:
- 左边是语法节点
- 右边是语义符号
如果没有它,你就只能停留在“这看起来像一个类”的层面,拿不到真正能用于代码生成的类型信息。
零基础阶段先记 3 个最常见调用就够了:
GetDeclaredSymbol(...)- 从“声明节点”拿到“声明对应的符号”
GetTypeInfo(...)- 从某个节点拿到它的类型信息
GetSymbolInfo(...)- 从某次名字引用或调用位置,拿到它实际指向哪个符号
当前主案例最关键的是第一个,因为它正好完成了:
ClassDeclarationSyntax- 到
INamedTypeSymbol
这一步转换。
为什么源生成器不能只靠语法
因为很多真实问题都不是“看起来像什么”,而是“它实际是什么”。
比如:
- 一个属性写成
string?,你要判断它是不是可空引用类型 - 一个类写成
Container<T>,你要知道它是不是泛型类,以及类型参数有哪些 - 一个嵌套类
Outer.Inner,你要知道它的外层类型是什么
这些都不是只靠 SyntaxNode 就能稳妥拿全的。
如何验证
按下面顺序做:
- 打开
sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs - 找到
TransformClass(...) - 圈出“语法对象”所在的代码
- 圈出“语义对象”所在的代码
- 试着回答:如果把
SemanticModel那一行删掉,后面哪些信息你就很难拿到了?
如果你能回答这个问题,就说明你已经真正理解“为什么只看语法不够”。
动手练习
- 给
TransformClass(...)写一份你自己的读码注释,标明哪几行是语法处理,哪几行是语义处理 - 用一句话分别解释:什么是语法,什么是语义
- 找一个你熟悉的类,试着想想:只看源码文本时你拿不到哪些真正重要的信息
常见误解
- 误解 1:语法和语义只是两个专业词,其实差不多
- 不一样,语法关心“写出来是什么样”,语义关心“它真正代表什么”
- 误解 2:只要有
ClassDeclarationSyntax,就等于拿到了类信息- 还远远不够,很多关键能力来自语义符号
- 误解 3:
SemanticModel是高级技巧,零基础阶段不用管- 恰恰相反,它是理解源生成器为何能“看懂代码”的关键一层
本章新名词
SyntaxNodeClassDeclarationSyntaxSemanticModelINamedTypeSymbol
本章小结
这一章最重要的不是记住对象名字,而是建立一个稳定分工:语法负责筛选,语义负责理解。
下一章我们就进入最实际的一步:从语义对象里到底能拿出哪些类信息和属性信息。
上一章 | 返回主教程目录 | 下一章:读取类和属性信息