Skip to content

第 7 章:语法与语义

本章目标

  • 彻底区分“语法”和“语义”
  • 知道为什么源生成器通常既要看语法,也要看语义
  • 理解 ClassDeclarationSyntaxINamedTypeSymbol 的角色分工

先看现象

如果你只拿到一段 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 就能稳妥拿全的。

如何验证

按下面顺序做:

  1. 打开 sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs
  2. 找到 TransformClass(...)
  3. 圈出“语法对象”所在的代码
  4. 圈出“语义对象”所在的代码
  5. 试着回答:如果把 SemanticModel 那一行删掉,后面哪些信息你就很难拿到了?

如果你能回答这个问题,就说明你已经真正理解“为什么只看语法不够”。

动手练习

  1. TransformClass(...) 写一份你自己的读码注释,标明哪几行是语法处理,哪几行是语义处理
  2. 用一句话分别解释:什么是语法,什么是语义
  3. 找一个你熟悉的类,试着想想:只看源码文本时你拿不到哪些真正重要的信息

常见误解

  • 误解 1:语法和语义只是两个专业词,其实差不多
    • 不一样,语法关心“写出来是什么样”,语义关心“它真正代表什么”
  • 误解 2:只要有 ClassDeclarationSyntax,就等于拿到了类信息
    • 还远远不够,很多关键能力来自语义符号
  • 误解 3:SemanticModel 是高级技巧,零基础阶段不用管
    • 恰恰相反,它是理解源生成器为何能“看懂代码”的关键一层

本章新名词

  • SyntaxNode
  • ClassDeclarationSyntax
  • SemanticModel
  • INamedTypeSymbol

本章小结

这一章最重要的不是记住对象名字,而是建立一个稳定分工:语法负责筛选,语义负责理解。

下一章我们就进入最实际的一步:从语义对象里到底能拿出哪些类信息和属性信息。

上一章 | 返回主教程目录 | 下一章:读取类和属性信息

基于当前仓库文档副本构建的 VitePress 站点