找回密码
 立即注册
首页 业界区 业界 C#源生成器:让你的代码飞起来的黑科技 ...

C#源生成器:让你的代码飞起来的黑科技

删一 2025-7-11 22:39:26
大家好,我是token。今天想和大家聊聊C#源生成器这个神奇的技术。
说起源生成器,可能很多同学会想:又是什么新的轮子?我反射用得好好的,为什么要学这个?别急,看完这篇文章,你就会发现源生成器简直是性能优化的救命稻草,能让你的应用快到飞起。
源生成器到底是个啥?

简单来说,源生成器就是一个在编译时帮你写代码的小助手。想象一下,你有一个非常勤快的实习生,每次编译的时候,他都会根据你的要求自动生成一堆代码,而且生成的代码质量还特别高。
传统的做法是什么样的呢?比如你想做个序列化:
  1. // 老式做法:运行时反射,慢得像蜗牛
  2. var json = JsonSerializer.Serialize(person); // 内部大量反射调用
复制代码
而源生成器的做法是:
  1. // 编译时就生成好了序列化代码,快得像火箭
  2. var json = JsonSerializer.Serialize(person, PersonContext.Default.Person);
复制代码
看起来差不多,但实际性能差了一个天地。
为什么源生成器这么快?

数据说话最有说服力。在序列化场景中,传统反射需要734.563纳秒,而源生成器只需要6.253纳秒。这是117倍的性能提升!
为什么会有这么大的差距呢?
反射的痛点:

  • 运行时才开始分析类型结构
  • 需要缓存和管理大量元数据
  • 每次调用都有装箱拆箱的开销
  • GC压力山大
源生成器的优势:

  • 编译时就把所有工作做完了
  • 生成的代码直接调用,没有中间层
  • 零反射,零装箱
  • 内存占用更低
就像是你要做一道菜,反射是现场买菜现场切,而源生成器是提前把所有食材都准备好,直接下锅。
第一个源生成器:Hello World

让我们来写一个最简单的源生成器。首先创建一个新的类库项目:
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.   <PropertyGroup>
  3.     <TargetFramework>netstandard2.0</TargetFramework>
  4.     <IsRoslynComponent>true</IsRoslynComponent>
  5.     <IncludeBuildOutput>false</IncludeBuildOutput>
  6.   </PropertyGroup>
  7.   
  8.   <ItemGroup>
  9.     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
  10.     <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  11.   </ItemGroup>
  12. </Project>
复制代码
然后写一个简单的生成器:
  1. [Generator]
  2. public class HelloWorldGenerator : ISourceGenerator
  3. {
  4.     public void Initialize(GeneratorInitializationContext context)
  5.     {
  6.         // 初始化,一般用不到
  7.     }
  8.     public void Execute(GeneratorExecutionContext context)
  9.     {
  10.         var sourceCode = @"
  11. namespace Generated
  12. {
  13.     public static class HelloWorld
  14.     {
  15.         public static string SayHello() => ""Hello from Source Generator!"";
  16.     }
  17. }";
  18.         context.AddSource("HelloWorld.g.cs", sourceCode);
  19.     }
  20. }
复制代码
在消费项目中引用这个生成器:
  1. [/code]现在你可以直接使用生成的代码:
  2. [code]using Generated;
  3. Console.WriteLine(HelloWorld.SayHello()); // 输出: Hello from Source Generator!
复制代码
进阶技巧:增量源生成器

上面的例子虽然能工作,但在大型项目中会有性能问题。每次编译都会重新生成所有代码,就像每次做饭都要重新洗所有的锅一样浪费。
这时候就要用到增量源生成器了。它采用了类似React的虚拟DOM的思想,只有变化的部分才会重新生成:
  1. [Generator]
  2. public class SmartPropertyGenerator : IIncrementalGenerator
  3. {
  4.     public void Initialize(IncrementalGeneratorInitializationContext context)
  5.     {
  6.         // 只关注有特定属性的类
  7.         var pipeline = context.SyntaxProvider
  8.             .ForAttributeWithMetadataName(
  9.                 "MyNamespace.GeneratePropertiesAttribute",
  10.                 predicate: static (node, _) => node is ClassDeclarationSyntax,
  11.                 transform: static (ctx, _) => GetClassInfo(ctx))
  12.             .Where(static m => m is not null);
  13.         
  14.         context.RegisterSourceOutput(pipeline, GenerateProperties);
  15.     }
  16.    
  17.     private static ClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context)
  18.     {
  19.         var classDeclaration = (ClassDeclarationSyntax)context.TargetNode;
  20.         var symbol = context.TargetSymbol as INamedTypeSymbol;
  21.         
  22.         return new ClassInfo(
  23.             Name: symbol.Name,
  24.             Namespace: symbol.ContainingNamespace.ToDisplayString()
  25.         );
  26.     }
  27.    
  28.     private static void GenerateProperties(SourceProductionContext context, ClassInfo classInfo)
  29.     {
  30.         var source = $@"
  31. namespace {classInfo.Namespace}
  32. {{
  33.     partial class {classInfo.Name}
  34.     {{
  35.         public string GeneratedProperty {{ get; set; }} = ""Auto-generated!"";
  36.     }}
  37. }}";
  38.         context.AddSource($"{classInfo.Name}.Properties.g.cs", source);
  39.     }
  40. }
  41. public record ClassInfo(string Name, string Namespace);
复制代码
使用的时候只需要加个属性:
  1. [GenerateProperties]
  2. public partial class Person
  3. {
  4.     public string Name { get; set; }
  5.     // GeneratedProperty 会被自动生成
  6. }
复制代码
实战案例:FastService的妙用

说到实际应用,不得不提一下FastService这个项目。它用源生成器简化了ASP.NET Core的API开发,让你写API就像写普通方法一样简单。
传统的Minimal API写法:
  1. app.MapGet("/api/users", async (UserService service) =>
  2. {
  3.     return await service.GetUsersAsync();
  4. });
  5. app.MapPost("/api/users", async (CreateUserRequest request, UserService service) =>
  6. {
  7.     return await service.CreateUserAsync(request);
  8. });
  9. // 还有一大堆路由配置...
复制代码
用了FastService之后:
  1. [Route("/api/users")]
  2. [Tags("用户管理")]
  3. public class UserService : FastApi
  4. {
  5.     [EndpointSummary("获取用户列表")]
  6.     public async Task<List<User>> GetUsersAsync()
  7.     {
  8.         return await GetAllUsersAsync();
  9.     }
  10.    
  11.     [EndpointSummary("创建用户")]
  12.     public async Task<User> CreateUserAsync(CreateUserRequest request)
  13.     {
  14.         return await SaveUserAsync(request);
  15.     }
  16. }
复制代码
源生成器会自动分析方法名,推断HTTP方法类型:

  • Get* → GET请求
  • Create*, Add*, Post* → POST请求
  • Update*, Edit*, Put* → PUT请求
  • Delete*, Remove* → DELETE请求
然后生成对应的路由注册代码。这样既保持了强类型的优势,又大大简化了代码编写。
性能优化的秘密武器

在开发源生成器时,有几个性能优化的小技巧:
1. 早期过滤

不要什么节点都分析,先用谓词函数过滤掉不需要的:
  1. var pipeline = context.SyntaxProvider
  2.     .CreateSyntaxProvider(
  3.         predicate: static (node, _) => node is ClassDeclarationSyntax cls &&
  4.                                       cls.AttributeLists.Count > 0, // 只看有属性的类
  5.         transform: static (ctx, _) => TransformNode(ctx))
复制代码
2. 使用值类型数据模型

千万不要在数据模型中保存Syntax或ISymbol对象,它们不能被正确缓存:
  1. // ❌ 错误做法
  2. public record ClassInfo(ClassDeclarationSyntax Syntax, INamedTypeSymbol Symbol);
  3. // ✅ 正确做法
  4. public readonly record struct ClassInfo(
  5.     string Name,
  6.     string Namespace,
  7.     EquatableArray<PropertyInfo> Properties);
复制代码
3. 对象池优化

对于频繁创建的对象,使用对象池:
  1. private static readonly ObjectPool<StringBuilder> _stringBuilderPool =
  2.     new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
  3. private static string GenerateCode(ClassInfo info)
  4. {
  5.     var sb = _stringBuilderPool.Get();
  6.     try
  7.     {
  8.         sb.AppendLine($"namespace {info.Namespace}");
  9.         // 生成代码...
  10.         return sb.ToString();
  11.     }
  12.     finally
  13.     {
  14.         _stringBuilderPool.Return(sb);
  15.     }
  16. }
复制代码
调试技巧:不再抓瞎

源生成器的调试曾经是个大难题,但现在有了不少好用的技巧。
1. 断点调试

在源生成器代码中加入:
  1. public void Initialize(IncrementalGeneratorInitializationContext context)
  2. {
  3. #if DEBUG
  4.     if (!Debugger.IsAttached)
  5.     {
  6.         Debugger.Launch(); // 会弹出调试器选择界面
  7.     }
  8. #endif
  9. }
复制代码
2. 查看生成的代码

在项目文件中加入:
  1. <PropertyGroup>
  2.   <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  3.   <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
  4. </PropertyGroup>
复制代码
编译后,生成的代码会保存在Generated文件夹中,你可以直接查看。
3. 单元测试

写个测试来验证生成器的行为:
  1. [Test]
  2. public void Should_Generate_Property_For_Marked_Class()
  3. {
  4.     var source = @"
  5. using MyNamespace;
  6. [GenerateProperties]
  7. public partial class TestClass
  8. {
  9. }";
  10.     var result = RunGenerator(source);
  11.    
  12.     Assert.That(result, Contains.Substring("public string GeneratedProperty"));
  13. }
复制代码
常见陷阱与避坑指南

开发源生成器时,有几个坑是新手经常掉进去的:
1. 命名空间冲突

生成的代码可能和现有代码冲突,记得加上合适的命名空间或者前缀:
  1. // 生成的代码加上特殊前缀
  2. var className = $"Generated_{originalClassName}";
复制代码
2. 编译错误处理

当生成的代码有语法错误时,编译器的错误信息可能不够清晰。建议在生成器中添加诊断信息:
  1. public static readonly DiagnosticDescriptor InvalidClassError = new(
  2.     id: "SG001",
  3.     title: "Invalid class for generation",
  4.     messageFormat: "The class '{0}' must be partial to use this generator",
  5.     category: "SourceGenerator",
  6.     DiagnosticSeverity.Error,
  7.     isEnabledByDefault: true);
  8. // 在生成器中使用
  9. context.ReportDiagnostic(Diagnostic.Create(InvalidClassError, location, className));
复制代码
3. 增量生成缓存失效

如果数据模型设计不当,可能导致缓存频繁失效:
  1. // ❌ 这样会导致缓存失效,因为Compilation对象每次都不同
  2. var hasRef = context.Compilation.Select(comp => comp.ReferencedAssemblyNames.Any(...));
  3. // ✅ 正确的做法
  4. var hasRef = context.CompilationProvider.Select(comp =>
  5.     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!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册