Skip to content

第 8 章:读取类和属性信息

本章目标

  • 看懂 TransformClass(...) 是如何提取类信息和属性信息的
  • 知道为什么需要 ClassToGeneratePropertyInfo 这两个中间模型
  • 能说清类名、命名空间、泛型信息、属性列表分别从哪里拿

先看现象

到这一章为止,你已经知道两件事:

  1. 生成器可以通过特性找到目标类
  2. 找到目标类后,不能只停留在语法层,还必须进入语义层

但还有一个关键问题没解决:

  • 生成器到底从目标类里拿走了哪些信息,才能生成后面的 ToString()

答案是:它不会直接边读边拼字符串,而是先把信息整理成中间模型。

再看代码

数据模型在这里:

  • sample/01-tostring-generator/ToStringGenerator/Models/ClassToGenerate.cs
  • sample/01-tostring-generator/ToStringGenerator/Models/PropertyInfo.cs

提取逻辑在这里:

  • sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs
  • TransformClass(...)

类级信息是怎么拿的

INamedTypeSymbol 上最常用的类级信息包括:

  • Name
    • 类名
  • ContainingNamespace
    • 命名空间
  • IsGenericType
    • 是否泛型类
  • TypeParameters
    • 泛型参数
  • ContainingType
    • 外层类型,用于判断嵌套类

主案例里正是通过这些信息来构建 ClassToGenerate

如果你第一次读 INamedTypeSymbol,最应该先盯住这几项:

  • Name
    • 当前类名
  • ContainingNamespace
    • 当前类属于哪个命名空间
  • IsGenericType
    • 当前类是不是泛型类
  • TypeParameters
    • 泛型参数有哪些
  • ContainingType
    • 当前类是不是嵌套在别的类里
  • GetMembers()
    • 类里到底有哪些成员

属性级信息是怎么拿的

主案例的属性提取逻辑大致是:

csharp
var properties = symbol.GetMembers()
    .OfType<IPropertySymbol>()
    .Where(p => p.DeclaredAccessibility == Accessibility.Public && p.GetMethod is not null)
    ...

这表示:

  1. 先拿到类里的所有成员
  2. 再筛出属性成员
  3. 再筛出“公共可读属性”

为什么是“公共可读属性”?因为对 ToString() 来说,这类属性最适合作为最终输出内容。

这里也可以顺手建立一个“成员怎么看”的最小映射:

  • p.Name
    • 属性名
  • p.Type
    • 属性类型
  • p.DeclaredAccessibility
    • 访问级别
  • p.GetMethod
    • 是否可读
  • p.SetMethod
    • 是否可写

p.Type 再往下走,又会进入 ITypeSymbol 这一层,继续判断:

  • ToDisplayString()
    • 最终类型名怎么显示
  • IsValueType
    • 是不是值类型
  • NullableAnnotation
    • 有没有可空标注
  • OriginalDefinition
    • 原始泛型定义是什么

为什么不直接边读边拼代码

如果你直接一边读取类信息、一边拼接 StringBuilder,会很快遇到这些问题:

  • 逻辑耦合太重
  • 难以测试
  • 难以扩展
  • 后续遇到泛型、嵌套类、null 处理时会越来越乱

中间模型的好处是:

  • 先把“提取信息”这一步独立出来
  • 再把“根据这些信息生成代码”作为下一步处理

这也是为什么主案例里会有:

  • ClassToGenerate
  • PropertyInfo

如何验证

按下面顺序做:

  1. 打开 ClassToGenerate.csPropertyInfo.cs
  2. 对照 TransformClass(...),确认每个字段从哪一段代码赋值
  3. Person 为例,自己口头走一遍:如果传给 TransformClass(...),最后会得到哪些类信息和哪些属性信息
  4. 再看 EmployeeContainer<T>,想想为什么同一个中间模型也能承载这些更复杂的情况

动手练习

  1. 列出 Person 最终会进入 ClassToGenerate 的 5 个字段
  2. 找出“公共可读属性”的完整筛选条件
  3. 用一句话说明:为什么 TransformClass(...) 不直接返回字符串,而要返回 ClassToGenerate

常见误解

  • 误解 1:中间模型只是为了代码看起来高级
    • 不是,它是在控制复杂度,避免提取逻辑和生成逻辑粘在一起
  • 误解 2:GetMembers() 拿到的就是“属性列表”
    • 不是,它拿到的是所有成员,你还要继续筛选
  • 误解 3:类名和属性名都是小信息,不需要专门整理
    • 对生成器来说,这些恰恰是最终代码生成最关键的输入数据

本章新名词

  • GetMembers()
  • IPropertySymbol
  • DeclaredAccessibility
  • GetMethod
  • ITypeSymbol

本章小结

到这里你已经真正站在“生成之前”的最后一步:你知道目标类怎么被找到,也知道生成器会从它身上提取出哪些信息。

里程碑 2:你已经能找到目标并读取信息

你现在应该具备的能力

  • 能解释特性在生成器里的入口作用
  • 能说清 ForAttributeWithMetadataName(...) 在做什么
  • 能区分语法信息和语义信息
  • 能指出类信息和属性信息分别从哪里拿
  • 能理解中间模型为什么必要

自检问题

  1. 为什么不能只靠字符串搜索 [GenerateToString]
  2. 为什么 ClassDeclarationSyntax 不能替代 INamedTypeSymbol
  3. TransformClass(...) 为什么输出的是 ClassToGenerate

下一章开始,我们终于把这些提取出来的信息接到真正的动态生成上,先从一个比完整 ToString() 更轻的中间态开始。

上一章 | 返回主教程目录 | 下一章:先生成一个简单方法

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