Appearance
第 8 章:读取类和属性信息
本章目标
- 看懂
TransformClass(...)是如何提取类信息和属性信息的 - 知道为什么需要
ClassToGenerate和PropertyInfo这两个中间模型 - 能说清类名、命名空间、泛型信息、属性列表分别从哪里拿
先看现象
到这一章为止,你已经知道两件事:
- 生成器可以通过特性找到目标类
- 找到目标类后,不能只停留在语法层,还必须进入语义层
但还有一个关键问题没解决:
- 生成器到底从目标类里拿走了哪些信息,才能生成后面的
ToString()?
答案是:它不会直接边读边拼字符串,而是先把信息整理成中间模型。
再看代码
数据模型在这里:
sample/01-tostring-generator/ToStringGenerator/Models/ClassToGenerate.cssample/01-tostring-generator/ToStringGenerator/Models/PropertyInfo.cs
提取逻辑在这里:
sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.csTransformClass(...)
类级信息是怎么拿的
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)
...这表示:
- 先拿到类里的所有成员
- 再筛出属性成员
- 再筛出“公共可读属性”
为什么是“公共可读属性”?因为对 ToString() 来说,这类属性最适合作为最终输出内容。
这里也可以顺手建立一个“成员怎么看”的最小映射:
p.Name- 属性名
p.Type- 属性类型
p.DeclaredAccessibility- 访问级别
p.GetMethod- 是否可读
p.SetMethod- 是否可写
而 p.Type 再往下走,又会进入 ITypeSymbol 这一层,继续判断:
ToDisplayString()- 最终类型名怎么显示
IsValueType- 是不是值类型
NullableAnnotation- 有没有可空标注
OriginalDefinition- 原始泛型定义是什么
为什么不直接边读边拼代码
如果你直接一边读取类信息、一边拼接 StringBuilder,会很快遇到这些问题:
- 逻辑耦合太重
- 难以测试
- 难以扩展
- 后续遇到泛型、嵌套类、null 处理时会越来越乱
中间模型的好处是:
- 先把“提取信息”这一步独立出来
- 再把“根据这些信息生成代码”作为下一步处理
这也是为什么主案例里会有:
ClassToGeneratePropertyInfo
如何验证
按下面顺序做:
- 打开
ClassToGenerate.cs和PropertyInfo.cs - 对照
TransformClass(...),确认每个字段从哪一段代码赋值 - 以
Person为例,自己口头走一遍:如果传给TransformClass(...),最后会得到哪些类信息和哪些属性信息 - 再看
Employee、Container<T>,想想为什么同一个中间模型也能承载这些更复杂的情况
动手练习
- 列出
Person最终会进入ClassToGenerate的 5 个字段 - 找出“公共可读属性”的完整筛选条件
- 用一句话说明:为什么
TransformClass(...)不直接返回字符串,而要返回ClassToGenerate
常见误解
- 误解 1:中间模型只是为了代码看起来高级
- 不是,它是在控制复杂度,避免提取逻辑和生成逻辑粘在一起
- 误解 2:
GetMembers()拿到的就是“属性列表”- 不是,它拿到的是所有成员,你还要继续筛选
- 误解 3:类名和属性名都是小信息,不需要专门整理
- 对生成器来说,这些恰恰是最终代码生成最关键的输入数据
本章新名词
GetMembers()IPropertySymbolDeclaredAccessibilityGetMethodITypeSymbol
本章小结
到这里你已经真正站在“生成之前”的最后一步:你知道目标类怎么被找到,也知道生成器会从它身上提取出哪些信息。
里程碑 2:你已经能找到目标并读取信息
你现在应该具备的能力
- 能解释特性在生成器里的入口作用
- 能说清
ForAttributeWithMetadataName(...)在做什么 - 能区分语法信息和语义信息
- 能指出类信息和属性信息分别从哪里拿
- 能理解中间模型为什么必要
自检问题
- 为什么不能只靠字符串搜索
[GenerateToString]? - 为什么
ClassDeclarationSyntax不能替代INamedTypeSymbol? TransformClass(...)为什么输出的是ClassToGenerate?
下一章开始,我们终于把这些提取出来的信息接到真正的动态生成上,先从一个比完整 ToString() 更轻的中间态开始。
上一章 | 返回主教程目录 | 下一章:先生成一个简单方法