Skip to content

第 9 章:先生成一个简单方法

本章目标

  • 在进入完整 ToString() 之前,先理解动态生成的最小版本
  • 理解“教学中间态”和“最终实现”的关系
  • 把“读取信息”这一步真正接到“输出代码”这一步上

本章在主线里的位置

  • 对应 Level 1:简单动态生成
  • 这一章的重点不是完整功能,而是先看懂“输入变化,输出也跟着变化”

先看现象

如果你一上来就直接读完整 ToString() 的生成逻辑,很容易被这些细节打断:

  • null 处理
  • 值类型和引用类型差异
  • 泛型参数
  • 泛型约束
  • 嵌套类结构

所以这一章先做一个“中间态”:

csharp
public partial class Person
{
    public string GetGeneratedTypeName() => "Person";
}

这个例子比完整 ToString() 简单得多,但它已经完整包含了动态生成最核心的一步:

  • 先拿到类信息
  • 再根据类信息生成不同的代码

再看代码

主案例里真正的输出阶段在:

  • sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs

关键连接点是:

csharp
context.RegisterSourceOutput(classDeclarations, (spc, classInfo) =>
{
    if (classInfo is not null)
    {
        GenerateToStringCode(spc, classInfo);
    }
});

你可以先把它拆开理解:

  1. classDeclarations
    • 前面筛出来并转换好的目标数据
  2. spc
    • 输出上下文,也就是后面真正调用 AddSource(...) 的地方
  3. classInfo
    • 当前要处理的那一个类的信息

为什么这一章要先用“简单方法”做中间态

因为你现在已经会:

  • 找目标类
  • 拿类信息

但如果此时直接进入完整 ToString(),你会同时处理太多维度。用 GetGeneratedTypeName() 这种简化版做过渡,可以把注意力集中在一件事上:

  • “拿到数据之后,怎么把它变成一段新的源码”

动态生成和固定输出的本质差别

前一阶段的固定输出是:

  • 不管输入是什么
  • 输出基本都一样

这一章的动态输出是:

  • 输入不同
  • 输出也要跟着不同

比如:

  • Person 生成 GetGeneratedTypeName() => "Person"
  • Employee 生成 GetGeneratedTypeName() => "Employee"

如何验证

按下面顺序做:

  1. 回到 RegisterSourceOutput(...) 那段代码,确认 classInfo 是怎么流入输出阶段的
  2. 试着只使用 classInfo.ClassName,在脑中构造一份最小生成代码
  3. 对比固定输出章节想一想:现在多出来的那份输入是什么
  4. 再回头看完整案例,理解这一章的中间态只是教学过渡,最终实现仍然会回到真实的 ToString()

动手练习

  1. 自己写一版最小的 GetGeneratedTypeName() 代码草稿
  2. 用一句话解释:为什么这个中间态不需要属性列表
  3. 再用一句话解释:从固定输出到动态输出,最关键新增的输入是什么

常见误解

  • 误解 1:教学中间态不是仓库最终代码,所以没必要看
    • 恰恰相反,它是帮助你平滑跨过“第一次动态输出”这道坎的
  • 误解 2:只要会 AddSource(...) 就已经掌握动态生成
    • 还不够,关键是你要知道“根据什么数据生成不同输出”
  • 误解 3:RegisterSourceOutput(...) 只是个包裹函数
    • 它实际上是把前面的分析阶段和后面的输出阶段连接起来的关键点

本章新名词

  • RegisterSourceOutput(...)
  • SourceProductionContext
  • 动态输出
  • 教学中间态

本章小结

你已经跨过最重要的一步:从“拿到信息”走到了“根据信息生成不同代码”。

下一章开始,我们把这条动态生成链路加上真实世界里的复杂度:null、泛型、嵌套类和类型差异。

上一章 | 返回主教程目录 | 下一章:处理复杂类型和边界情况

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