大家好,我是token。今天想和大家聊聊C#源生成器这个神奇的技术。
说起源生成器,可能很多同学会想:又是什么新的轮子?我反射用得好好的,为什么要学这个?别急,看完这篇文章,你就会发现源生成器简直是性能优化的救命稻草,能让你的应用快到飞起。
源生成器到底是个啥?
简单来说,源生成器就是一个在编译时帮你写代码的小助手。想象一下,你有一个非常勤快的实习生,每次编译的时候,他都会根据你的要求自动生成一堆代码,而且生成的代码质量还特别高。
传统的做法是什么样的呢?比如你想做个序列化:
// 老式做法:运行时反射,慢得像蜗牛 var json = JsonSerializer.Serialize(person); // 内部大量反射调用
而源生成器的做法是:
// 编译时就生成好了序列化代码,快得像火箭 var json = JsonSerializer.Serialize(person, PersonContext.Default.Person);
看起来差不多,但实际性能差了一个天地。
为什么源生成器这么快?
数据说话最有说服力。在序列化场景中,传统反射需要734.563纳秒,而源生成器只需要6.253纳秒。这是117倍的性能提升!
为什么会有这么大的差距呢?
反射的痛点:
- 运行时才开始分析类型结构
- 需要缓存和管理大量元数据
- 每次调用都有装箱拆箱的开销
- GC压力山大
源生成器的优势:
- 编译时就把所有工作做完了
- 生成的代码直接调用,没有中间层
- 零反射,零装箱
- 内存占用更低
就像是你要做一道菜,反射是现场买菜现场切,而源生成器是提前把所有食材都准备好,直接下锅。
第一个源生成器:Hello World
让我们来写一个最简单的源生成器。首先创建一个新的类库项目:
<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>
然后写一个简单的生成器:
[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); } }
在消费项目中引用这个生成器:
<ProjectReference Include="../HelloWorldGenerator/HelloWorldGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
现在你可以直接使用生成的代码:
using Generated; Console.WriteLine(HelloWorld.SayHello()); // 输出: Hello from Source Generator!
进阶技巧:增量源生成器
上面的例子虽然能工作,但在大型项目中会有性能问题。每次编译都会重新生成所有代码,就像每次做饭都要重新洗所有的锅一样浪费。
这时候就要用到增量源生成器了。它采用了类似React的虚拟DOM的思想,只有变化的部分才会重新生成:
[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);
使用的时候只需要加个属性:
[GenerateProperties] public partial class Person { public string Name { get; set; } // GeneratedProperty 会被自动生成 }
实战案例:FastService的妙用
说到实际应用,不得不提一下FastService这个项目。它用源生成器简化了ASP.NET Core的API开发,让你写API就像写普通方法一样简单。
传统的Minimal API写法:
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之后:
[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. 早期过滤
不要什么节点都分析,先用谓词函数过滤掉不需要的:
var pipeline = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (node, _) => node is ClassDeclarationSyntax cls && cls.AttributeLists.Count > 0, // 只看有属性的类 transform: static (ctx, _) => TransformNode(ctx))
2. 使用值类型数据模型
千万不要在数据模型中保存Syntax或ISymbol对象,它们不能被正确缓存:
// ❌ 错误做法 public record ClassInfo(ClassDeclarationSyntax Syntax, INamedTypeSymbol Symbol); // ✅ 正确做法 public readonly record struct ClassInfo( string Name, string Namespace, EquatableArray<PropertyInfo> Properties);
3. 对象池优化
对于频繁创建的对象,使用对象池:
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. 断点调试
在源生成器代码中加入:
public void Initialize(IncrementalGeneratorInitializationContext context) { #if DEBUG if (!Debugger.IsAttached) { Debugger.Launch(); // 会弹出调试器选择界面 } #endif }
2. 查看生成的代码
在项目文件中加入:
<PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup>
编译后,生成的代码会保存在Generated
文件夹中,你可以直接查看。
3. 单元测试
写个测试来验证生成器的行为:
[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. 命名空间冲突
生成的代码可能和现有代码冲突,记得加上合适的命名空间或者前缀:
// 生成的代码加上特殊前缀 var className = $"Generated_{originalClassName}";
2. 编译错误处理
当生成的代码有语法错误时,编译器的错误信息可能不够清晰。建议在生成器中添加诊断信息:
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. 增量生成缓存失效
如果数据模型设计不当,可能导致缓存频繁失效:
// ❌ 这样会导致缓存失效,因为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!
原创作者: token-ai 转载于: https://www.cnblogs.com/token-ai/p/18979978