Appearance
源码导读:主案例从哪里看起
这页专门解决一个问题:网页文档讲了很多概念,但如果你不知道源码具体在哪、先看什么、每个文件负责什么,就很容易失去方向。
先说明一个约定:
- 本站里出现的
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 个阶段:
GenerateAttribute(context)- 先生成
[GenerateToString]特性定义
- 先生成
ForAttributeWithMetadataName(...)- 找出所有带这个特性的类
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 - 用输入源码跑一遍生成过程
- 然后断言生成结果和诊断信息
这也是为什么测试工程很重要。它能验证“有没有生成对”,而不是只验证“程序能不能跑”。
一条完整链路
把整个项目串起来,就是这条线:
- 你在
SimpleClass.cs里写[GenerateToString] public partial class Person ToStringGenerator.Sample.csproj通过 Analyzer 方式接入生成器- 编译时
ToStringGenerator.cs找到Person - 生成器输出
Person.g.cs - 编译器把手写
Person和生成的Person合并 Program.cs运行时就能直接调用person.ToString()ToStringGenerator.Tests再对生成结果做自动化断言
如果这 7 步你已经能串起来,整个案例的骨架就清楚了。
推荐阅读顺序
第一次阅读建议严格按这个顺序:
sample/01-tostring-generator/ToStringGenerator.Sample/Examples/SimpleClass.cssample/01-tostring-generator/ToStringGenerator.Sample/Program.cssample/01-tostring-generator/ToStringGenerator.Sample/ToStringGenerator.Sample.csprojsample/01-tostring-generator/ToStringGenerator/ToStringGenerator.cssample/01-tostring-generator/ToStringGenerator.Sample/obj/Debug/net8.0/generated/...sample/01-tostring-generator/ToStringGenerator.Tests/TestHelpers.cs
不要一开始就从 ToStringGenerator.cs 第一行硬读到最后一行。那样你会先淹没在 API 细节里,而不是先看懂整条链路。
和主教程怎么配合
- 学第 1-3 章前后,先看这页建立源码地图
- 学第 4-11 章时,反复回到
ToStringGenerator.cs对照章节内容 - 学第 12 章时,再回来看
generated目录和TestHelpers.cs
返回首页 | 进入主教程 | 查看附录 A:文件路径导航