大家好,我是token。今天想和大家聊聊C#源生成器这个神奇的技术。
说起源生成器,可能很多同学会想:又是什么新的轮子?我反射用得好好的,为什么要学这个?别急,看完这篇文章,你就会发现源生成器简直是性能优化的救命稻草,能让你的应用快到飞起。
源生成器到底是个啥?
简单来说,源生成器就是一个在编译时帮你写代码的小助手。想象一下,你有一个非常勤快的实习生,每次编译的时候,他都会根据你的要求自动生成一堆代码,而且生成的代码质量还特别高。
传统的做法是什么样的呢?比如你想做个序列化:
csharp
// 老式做法:运行时反射,慢得像蜗牛
var json = JsonSerializer.Serialize(person); // 内部大量反射调用
而源生成器的做法是:
csharp
// 编译时就生成好了序列化代码,快得像火箭
var json = JsonSerializer.Serialize(person, PersonContext.Default.Person);
看起来差不多,但实际性能差了一个天地。
为什么源生成器这么快?
数据说话最有说服力。在序列化场景中,传统反射需要734.563纳秒,而源生成器只需要6.253纳秒。这是117倍的性能提升!
为什么会有这么大的差距呢?
反射的痛点:
- 运行时才开始分析类型结构
- 需要缓存和管理大量元数据
- 每次调用都有装箱拆箱的开销
- GC压力山大
源生成器的优势:
- 编译时就把所有工作做完了
- 生成的代码直接调用,没有中间层
- 零反射,零装箱
- 内存占用更低
就像是你要做一道菜,反射是现场买菜现场切,而源生成器是提前把所有食材都准备好,直接下锅。
第一个源生成器:Hello World
让我们来写一个最简单的源生成器。首先创建一个新的类库项目:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
然后写一个简单的生成器:
csharp
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 初始化,一般用不到
}
public void Execute(GeneratorExecutionContext context)
{
var sourceCode = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""Hello from Source Generator!"";
}
}";
context.AddSource("HelloWorld.g.cs", sourceCode);
}
}
在消费项目中引用这个生成器:
xml
<ProjectReference Include="../HelloWorldGenerator/HelloWorldGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
现在你可以直接使用生成的代码:
csharp
using Generated;
Console.WriteLine(HelloWorld.SayHello()); // 输出: Hello from Source Generator!
进阶技巧:增量源生成器
上面的例子虽然能工作,但在大型项目中会有性能问题。每次编译都会重新生成所有代码,就像每次做饭都要重新洗所有的锅一样浪费。
这时候就要用到增量源生成器了。它采用了类似React的虚拟DOM的思想,只有变化的部分才会重新生成:
csharp
[Generator]
public class SmartPropertyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 只关注有特定属性的类
var pipeline = context.SyntaxProvider
.ForAttributeWithMetadataName(
"MyNamespace.GeneratePropertiesAttribute",
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetClassInfo(ctx))
.Where(static m => m is not null);
context.RegisterSourceOutput(pipeline, GenerateProperties);
}
private static ClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.TargetNode;
var symbol = context.TargetSymbol as INamedTypeSymbol;
return new ClassInfo(
Name: symbol.Name,
Namespace: symbol.ContainingNamespace.ToDisplayString()
);
}
private static void GenerateProperties(SourceProductionContext context, ClassInfo classInfo)
{
var source = $@"
namespace {classInfo.Namespace}
{{
partial class {classInfo.Name}
{{
public string GeneratedProperty {{ get; set; }} = ""Auto-generated!"";
}}
}}";
context.AddSource($"{classInfo.Name}.Properties.g.cs", source);
}
}
public record ClassInfo(string Name, string Namespace);
使用的时候只需要加个属性:
csharp
[GenerateProperties]
public partial class Person
{
public string Name { get; set; }
// GeneratedProperty 会被自动生成
}
实战案例:FastService的妙用
说到实际应用,不得不提一下FastService这个项目。它用源生成器简化了ASP.NET Core的API开发,让你写API就像写普通方法一样简单。
传统的Minimal API写法:
csharp
app.MapGet("/api/users", async (UserService service) =>
{
return await service.GetUsersAsync();
});
app.MapPost("/api/users", async (CreateUserRequest request, UserService service) =>
{
return await service.CreateUserAsync(request);
});
// 还有一大堆路由配置...
用了FastService之后:
csharp
[Route("/api/users")]
[Tags("用户管理")]
public class UserService : FastApi
{
[EndpointSummary("获取用户列表")]
public async Task<List<User>> GetUsersAsync()
{
return await GetAllUsersAsync();
}
[EndpointSummary("创建用户")]
public async Task<User> CreateUserAsync(CreateUserRequest request)
{
return await SaveUserAsync(request);
}
}
源生成器会自动分析方法名,推断HTTP方法类型:
Get*
→ GET请求Create*
,Add*
,Post*
→ POST请求Update*
,Edit*
,Put*
→ PUT请求Delete*
,Remove*
→ DELETE请求
然后生成对应的路由注册代码。这样既保持了强类型的优势,又大大简化了代码编写。
性能优化的秘密武器
在开发源生成器时,有几个性能优化的小技巧:
1. 早期过滤
不要什么节点都分析,先用谓词函数过滤掉不需要的:
csharp
var pipeline = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax cls &&
cls.AttributeLists.Count > 0, // 只看有属性的类
transform: static (ctx, _) => TransformNode(ctx))
2. 使用值类型数据模型
千万不要在数据模型中保存Syntax或ISymbol对象,它们不能被正确缓存:
csharp
// ❌ 错误做法
public record ClassInfo(ClassDeclarationSyntax Syntax, INamedTypeSymbol Symbol);
// ✅ 正确做法
public readonly record struct ClassInfo(
string Name,
string Namespace,
EquatableArray<PropertyInfo> Properties);
3. 对象池优化
对于频繁创建的对象,使用对象池:
csharp
private static readonly ObjectPool<StringBuilder> _stringBuilderPool =
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
private static string GenerateCode(ClassInfo info)
{
var sb = _stringBuilderPool.Get();
try
{
sb.AppendLine($"namespace {info.Namespace}");
// 生成代码...
return sb.ToString();
}
finally
{
_stringBuilderPool.Return(sb);
}
}
调试技巧:不再抓瞎
源生成器的调试曾经是个大难题,但现在有了不少好用的技巧。
1. 断点调试
在源生成器代码中加入:
csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch(); // 会弹出调试器选择界面
}
#endif
}
2. 查看生成的代码
在项目文件中加入:
xml
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
编译后,生成的代码会保存在Generated
文件夹中,你可以直接查看。
3. 单元测试
写个测试来验证生成器的行为:
csharp
[Test]
public void Should_Generate_Property_For_Marked_Class()
{
var source = @"
using MyNamespace;
[GenerateProperties]
public partial class TestClass
{
}";
var result = RunGenerator(source);
Assert.That(result, Contains.Substring("public string GeneratedProperty"));
}
常见陷阱与避坑指南
开发源生成器时,有几个坑是新手经常掉进去的:
1. 命名空间冲突
生成的代码可能和现有代码冲突,记得加上合适的命名空间或者前缀:
csharp
// 生成的代码加上特殊前缀
var className = $"Generated_{originalClassName}";
2. 编译错误处理
当生成的代码有语法错误时,编译器的错误信息可能不够清晰。建议在生成器中添加诊断信息:
csharp
public static readonly DiagnosticDescriptor InvalidClassError = new(
id: "SG001",
title: "Invalid class for generation",
messageFormat: "The class '{0}' must be partial to use this generator",
category: "SourceGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
// 在生成器中使用
context.ReportDiagnostic(Diagnostic.Create(InvalidClassError, location, className));
3. 增量生成缓存失效
如果数据模型设计不当,可能导致缓存频繁失效:
csharp
// ❌ 这样会导致缓存失效,因为Compilation对象每次都不同
var hasRef = context.Compilation.Select(comp => comp.ReferencedAssemblyNames.Any(...));
// ✅ 正确的做法
var hasRef = context.CompilationProvider.Select(comp =>
comp.ReferencedAssemblyNames.Select(name => name.Name).OrderBy(x => x).ToArray());
生态系统现状
目前已经有不少成熟的源生成器项目:
- System.Text.Json - 微软官方的JSON序列化优化
- Mapperly - 对象映射生成器
- FastService - API开发简化
- StronglyTypedId - 强类型ID生成
- Meziantou.Framework.StronglyTypedId - 另一个强类型ID实现
这些项目都是学习源生成器的好例子,推荐大家去看看源码。
总结
源生成器真的是一个很酷的技术。它不仅能大幅提升应用性能,还能让我们写出更简洁、更高效的代码。虽然学习曲线有点陡峭,但一旦掌握了,你会发现很多以前觉得复杂的问题都能用源生成器优雅地解决。
如果你还在用传统的反射做序列化、映射这些工作,不妨试试源生成器。相信我,一旦体验过那种编译时生成代码的快感,你就再也回不去了。
最后,学习新技术最好的方法就是动手实践。建议大家从简单的Hello World开始,然后逐步尝试更复杂的场景。记住,代码是写给人看的,源生成器也不例外。写出清晰、可维护的生成器代码,比写出复杂炫技的代码更有价值。
Happy coding!