大家好,我是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!
浙公網安備 33010602011771號