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

大家好,我是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!