Skip to content

源码导读:主案例从哪里看起

这页专门解决一个问题:网页文档讲了很多概念,但如果你不知道源码具体在哪、先看什么、每个文件负责什么,就很容易失去方向。

先说明一个约定:

  • 本站里出现的 sample/...docs/... 都表示“相对于仓库根目录的路径”
  • 它们不是相对于当前网页文件的路径
  • 如果你在本地仓库中阅读代码,请从仓库根目录开始定位

主案例目录结构

整套教程围绕这个案例展开:

  • sample/01-tostring-generator

先建立目录直觉:

text
sample/01-tostring-generator
├─ ToStringGenerator
│  ├─ ToStringGenerator.cs
│  └─ Models
│     ├─ ClassToGenerate.cs
│     └─ PropertyInfo.cs
├─ ToStringGenerator.Sample
│  ├─ ToStringGenerator.Sample.csproj
│  ├─ Program.cs
│  └─ Examples
│     ├─ SimpleClass.cs
│     ├─ ComplexClass.cs
│     ├─ GenericClass.cs
│     ├─ NestedClass.cs
│     └─ EmptyClass.cs
└─ ToStringGenerator.Tests
   ├─ TestHelpers.cs
   └─ ToStringGeneratorTests.cs

先看哪 6 个文件

如果你是第一次读这个项目,先只盯住下面 6 个文件:

仓库路径为什么先看它
sample/01-tostring-generator/ToStringGenerator.Sample/Examples/SimpleClass.cs看“用户只写了什么”
sample/01-tostring-generator/ToStringGenerator.Sample/Program.cs看“最后运行时发生了什么”
sample/01-tostring-generator/ToStringGenerator.Sample/ToStringGenerator.Sample.csproj看“生成器是怎么接进编译流程的”
sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs看“生成器本体怎么工作”
sample/01-tostring-generator/ToStringGenerator.Sample/obj/Debug/net8.0/generated/.../Person.g.cs看“最终实际生成出来的代码”
sample/01-tostring-generator/ToStringGenerator.Tests/TestHelpers.cs看“测试怎么手动驱动生成器”

1. 用户实际手写了什么

仓库路径:

  • sample/01-tostring-generator/ToStringGenerator.Sample/Examples/SimpleClass.cs

源码:

csharp
#nullable enable

namespace ToStringGenerator.Sample.Examples;

[GenerateToString]
public partial class Person
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}

这里最关键的不是属性,而是这两个标记:

  • [GenerateToString]
  • partial

它们的含义是:

  • [GenerateToString] 表示“这是生成器要处理的目标类”
  • partial 表示“这个类的定义允许被拆到别的文件里补充”

你在这个文件里看不到 ToString(),这正是源生成器要解决的问题。

2. 程序运行时为什么能调用到 ToString

仓库路径:

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

你先不用看完整文件,只看最核心的一段:

csharp
var person = new Person
{
    Name = "张三",
    Age = 30
};
Console.WriteLine(person.ToString());

这段代码说明两件事:

  • Program.cs 像普通业务代码一样直接 new 出 Person
  • 它直接调用 person.ToString(),没有做反射,也没有手写扩展方法

所以真正的问题就变成:

Person 明明没手写 ToString(),为什么运行时却能调用到?

答案是:编译阶段,生成器已经把这个方法补进去了。

3. 生成器是怎么接进示例工程的

仓库路径:

  • sample/01-tostring-generator/ToStringGenerator.Sample/ToStringGenerator.Sample.csproj

最关键的是这段:

xml
<ProjectReference Include="..\ToStringGenerator\ToStringGenerator.csproj"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />

读法要分开:

  • ProjectReference
    • 引用另一个项目
  • OutputItemType="Analyzer"
    • 告诉编译器:这个引用不是普通类库,而是要参与编译分析的组件
  • ReferenceOutputAssembly="false"
    • 不把生成器当成普通运行时程序集引用进业务代码

如果没有这一段,示例工程只会编译自己的代码,不会触发源生成器。

4. 生成器本体做了什么

仓库路径:

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

先抓主线,不要一次读完整个文件。你只需要先理解 Initialize(...)

csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    GenerateAttribute(context);

    var classDeclarations = context.SyntaxProvider
        .ForAttributeWithMetadataName(
            fullyQualifiedMetadataName: "ToStringGenerator.GenerateToStringAttribute",
            predicate: IsClassDeclaration,
            transform: TransformClass)
        .Where(classInfo => classInfo is not null);

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

这段代码对应 3 个阶段:

  1. GenerateAttribute(context)
    • 先生成 [GenerateToString] 特性定义
  2. ForAttributeWithMetadataName(...)
    • 找出所有带这个特性的类
  3. RegisterSourceOutput(...)
    • 把这些类转换成真正的 .g.cs 源码输出

如果你把整个生成器只压缩成一句话,就是:

先注册入口特性,再筛目标类,再把类信息转成源码。

5. 生成结果实际长什么样

构建后可以去看:

  • sample/01-tostring-generator/ToStringGenerator.Sample/obj/Debug/net8.0/generated

例如 Person.g.cs 的核心内容是:

csharp
namespace ToStringGenerator.Sample.Examples
{
    public partial class Person
    {
        public override string ToString()
        {
            return $"Person {{ Name = {Name?.ToString() ?? "null"}, Age = {Age} }}";
        }
    }
}

这时候你应该把两份代码对起来看:

  • 手写文件里只有 Person 的属性定义
  • 生成文件里补上了 override string ToString()

它们因为都是 partial class Person,所以会在编译时合并成一个完整类型。

6. 测试工程是怎么验证生成器的

仓库路径:

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

测试工程不是靠运行示例程序做肉眼检查,而是直接在内存里驱动生成器:

csharp
var generator = new ToStringGenerator();
var driver = CSharpGeneratorDriver.Create(generator);

driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation(
    compilation,
    out var outputCompilation,
    out var diagnostics);

这段代码的意义是:

  • 手动创建生成器实例
  • 手动创建 Roslyn GeneratorDriver
  • 用输入源码跑一遍生成过程
  • 然后断言生成结果和诊断信息

这也是为什么测试工程很重要。它能验证“有没有生成对”,而不是只验证“程序能不能跑”。

一条完整链路

把整个项目串起来,就是这条线:

  1. 你在 SimpleClass.cs 里写 [GenerateToString] public partial class Person
  2. ToStringGenerator.Sample.csproj 通过 Analyzer 方式接入生成器
  3. 编译时 ToStringGenerator.cs 找到 Person
  4. 生成器输出 Person.g.cs
  5. 编译器把手写 Person 和生成的 Person 合并
  6. Program.cs 运行时就能直接调用 person.ToString()
  7. ToStringGenerator.Tests 再对生成结果做自动化断言

如果这 7 步你已经能串起来,整个案例的骨架就清楚了。

推荐阅读顺序

第一次阅读建议严格按这个顺序:

  1. sample/01-tostring-generator/ToStringGenerator.Sample/Examples/SimpleClass.cs
  2. sample/01-tostring-generator/ToStringGenerator.Sample/Program.cs
  3. sample/01-tostring-generator/ToStringGenerator.Sample/ToStringGenerator.Sample.csproj
  4. sample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cs
  5. sample/01-tostring-generator/ToStringGenerator.Sample/obj/Debug/net8.0/generated/...
  6. sample/01-tostring-generator/ToStringGenerator.Tests/TestHelpers.cs

不要一开始就从 ToStringGenerator.cs 第一行硬读到最后一行。那样你会先淹没在 API 细节里,而不是先看懂整条链路。

和主教程怎么配合

  • 学第 1-3 章前后,先看这页建立源码地图
  • 学第 4-11 章时,反复回到 ToStringGenerator.cs 对照章节内容
  • 学第 12 章时,再回来看 generated 目录和 TestHelpers.cs

返回首页 | 进入主教程 | 查看附录 A:文件路径导航

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