1. 介绍
写代码的时候,你可能会想:「为啥我又要一次次把同样的代码模板拷贝到不同的类里?」或者:「序列化、日志、数据映射——为什么要写这么多重复的行?」有时候真希望有人(或某个工具)能把这些繁琐的模板写好。
这时候 Source Generators 上场了——这是 C# 在 .NET 5 引入的新功能,现在还在不断发展。Source Generator 是个在编译阶段运行的库,它能动态生成 C# 代码,并在最终构建前把这些代码自动加入到你的项目里。
为啥要用它?
- 把重复工作自动化:省掉写重复类/方法的麻烦(boilerplate)。
- 编译器可检查的安全性:生成的代码会和你的代码一起编译(不像 T4 或 反射那样在运行时才出问题)。
- 高性能:序列化、DI、映射等可以避免运行时反射的开销。
- 支持现代模式:实现那些没有代码生成会很难或代价高的方案。
Source Generators 在“引擎盖”下怎么工作?
Source Generator 是个 .NET 库(通常是 Class Library 类型的项目),实现了接口 ISourceGenerator。在编译期间,Roslyn 会启动所有引入的生成器,给它们访问你代码的语法树(AST)的能力。
生成器会分析你的代码,决定该在哪儿生成什么内容,然后创建新的 C# 文件,编译器会立即把这些新文件编译进来。
自动生成 ToString
从简单的例子开始。假设有个属性很多的类,需要实现 ToString。手写看起来像这样:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
=> $"Person(Name={Name}, Age={Age})";
}
但当属性变多时你会懒得写,而且也容易忘记更新。Source Generator 可以替你做这件事!
2. 怎么创建自己的 Source Generator?
创建项目
打开 JetBrains Rider 或 Visual Studio,创建一个新的 Class Library (.NET Standard) 项目——这种项目可以做生成器。然后添加这些 NuGet 包:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
重要的属性
- [Generator] — 标记这个类是个 Source Generator。
最简单的生成器模板
这是一个最小可用的例子:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 可以注册额外的动作(可选)
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""嗨,世界!我被生成啦!"";
}
}";
context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
}
}
这个简单的生成器会在编译时总是添加一个静态类 HelloWorld,里面有个 SayHello 方法。
怎么在主项目里使用 Source Generators?
把生成器项目作为 NuGet 包或以 Project Reference 的形式引入,并把它放到 Analyzer 区块(详情见 官方文档)。
生成的代码会立刻在你的项目中可用——不用额外引用,直接用就行:
// 这将被自动生成!
using Generated;
Console.WriteLine(HelloWorld.SayHello());
3. 真实示例:自动生成 ToString
假设你希望所有带有属性 [AutoToString] 的类都自动获得 ToString 的实现。需要做的事:
- 创建一个自定义属性。
- 分析所有带这个属性的类。
- 为每个这样的类生成 ToString 方法。
属性
[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}
在代码中使用
[AutoToString]
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
简单的生成逻辑
生成器会查找带有 [AutoToString] 的类,并生成大概这样的代码:
public override string ToString()
=> $"Product(Name={Name}, Price={Price})";
生成器的真实片段
基本思路是用 Roslyn 遍历语法树:
public void Execute(GeneratorExecutionContext context)
{
// 分析所有语法树
foreach (var tree in context.Compilation.SyntaxTrees)
{
var root = tree.GetRoot();
// 找到所有带目标属性的类(简化示例)
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("AutoToString")));
foreach (var @class in classes)
{
var className = @class.Identifier.Text;
// 获取类的所有属性
var props = @class.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Identifier.Text)
.ToArray();
var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
var generated = $@"
partial class {className}
{{
public override string ToString() => $""{className}({toStringCode})"";
}}";
context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
}
}
}
注意:在生产代码里通常会通过 Roslyn 的 SemanticModel 做更准确的分析。
4. 有用的细节
需要注意的点
Source Generators 不能修改已有源码——只能创建新的文件(比如额外的 partial 类、方法等)。这意味着如果你的类声明为 partial,你可以为它生成额外的方法或属性。
有时候正确解析语法并处理语言的各种细节(嵌套类、泛型、修饰符等)并不容易。生成器作者需要保证生成的代码能编译通过,不会把项目弄坏。
另一个陷阱是:如果你生成的方法实现了接口,确保这些文件在每次构建时都被生成,否则可能出现奇怪的编译错误。现代工具通常能处理这些问题,但要记住这一点。
Source Generators 与 反射 的对比
反射:在运行时运行,开销大,编译器不能检查,处理大量数据时往往慢。
Source Generator:在编译阶段生成代码。所有东西都是静态可检查的,IDE 能看到生成的方法,自动补全也可用,性能和普通 C# 代码一致。
实际用途
- System.Text.Json:生成序列化/反序列化代码,避免反射。
- 设计 DI 容器:比如为 Microsoft.Extensions.DependencyInjection 生成依赖图。
- 像 Mapster 这样的映射库:从反射迁移到编译时生成 mapping 代码。
- 测试框架:基于属性自动生成测试方法。
- ASP.NET Minimal APIs(从 .NET 7 开始):生成 endpoint 处理器。
配置、参数和选项
生成器可以通过 MSBuild 参数、额外文件和约定来配置。例如,可以根据环境(Debug/Release)或应用配置生成不同的 ToString。
Source Generators 与真实任务的关系
对想写干净又快的代码的开发者来说,这是个很棒的工具:减少重复、增加编译期检查、IDE 提示更友好、重构更明确。面试里关于生成器的知识也越来越经常出现——从序列化和 DI 到映射都有涉及。
Source Generator 的生命周期
| 阶段 | 发生了什么 |
|---|---|
| 1. 项目被引入 | 你的生成器被作为 analyzer/reference 添加 |
| 2. Roslyn 编译源码 | 生成器获得 AST(抽象语法树) |
| 3. 生成器执行 | 添加新的 .cs 文件供编译使用 |
| 4. 所有东西被编译 | 生成的文件成为你构建的一部分 |
| 5. 代码就绪! | 生成的方法/类可以被调用 |
5. 调试和常见错误
新手写生成器时最常见的错误之一是忘了在你想要追加代码的类上写关键字 partial。如果不写 partial,编译器根本看不到你生成的内容。有时候生成的文件在 IDE 里不会立刻出现,需要第一次重建才会显示——别慌。
注意生成文件的命名:如果都用同一个 Name,它们会互相覆盖。小技巧:在文件名里加入类名,比如 context.AddSource($"{className}_ToString", ...)。
关于属性重复导入的错误——如果你生成了一个属性类,而主项目里已经有同名属性,会导致冲突。最好把需要的属性放到公共项目里,或者只在必要时生成属性代码。
GO TO FULL VERSION