大家好,我是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);
- }
- }
复制代码 在消费项目中引用这个生成器:- [/code]现在你可以直接使用生成的代码:
- [code]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!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |