大家好,我是token。今天想和大家聊聊C#源生成器這個(gè)神奇的技術(shù)。
說(shuō)起源生成器,可能很多同學(xué)會(huì)想:又是什么新的輪子?我反射用得好好的,為什么要學(xué)這個(gè)?別急,看完這篇文章,你就會(huì)發(fā)現(xiàn)源生成器簡(jiǎn)直是性能優(yōu)化的救命稻草,能讓你的應(yīng)用快到飛起。
源生成器到底是個(gè)啥?
簡(jiǎn)單來(lái)說(shuō),源生成器就是一個(gè)在編譯時(shí)幫你寫(xiě)代碼的小助手。想象一下,你有一個(gè)非常勤快的實(shí)習(xí)生,每次編譯的時(shí)候,他都會(huì)根據(jù)你的要求自動(dòng)生成一堆代碼,而且生成的代碼質(zhì)量還特別高。
傳統(tǒng)的做法是什么樣的呢?比如你想做個(gè)序列化:
// 老式做法:運(yùn)行時(shí)反射,慢得像蝸牛
var json = JsonSerializer.Serialize(person); // 內(nèi)部大量反射調(diào)用
而源生成器的做法是:
// 編譯時(shí)就生成好了序列化代碼,快得像火箭
var json = JsonSerializer.Serialize(person, PersonContext.Default.Person);
看起來(lái)差不多,但實(shí)際性能差了一個(gè)天地。
為什么源生成器這么快?
數(shù)據(jù)說(shuō)話最有說(shuō)服力。在序列化場(chǎng)景中,傳統(tǒng)反射需要734.563納秒,而源生成器只需要6.253納秒。這是117倍的性能提升!
為什么會(huì)有這么大的差距呢?
反射的痛點(diǎn):
- 運(yùn)行時(shí)才開(kāi)始分析類(lèi)型結(jié)構(gòu)
- 需要緩存和管理大量元數(shù)據(jù)
- 每次調(diào)用都有裝箱拆箱的開(kāi)銷(xiāo)
- GC壓力山大
源生成器的優(yōu)勢(shì):
- 編譯時(shí)就把所有工作做完了
- 生成的代碼直接調(diào)用,沒(méi)有中間層
- 零反射,零裝箱
- 內(nèi)存占用更低
就像是你要做一道菜,反射是現(xiàn)場(chǎng)買(mǎi)菜現(xiàn)場(chǎng)切,而源生成器是提前把所有食材都準(zhǔn)備好,直接下鍋。
第一個(gè)源生成器:Hello World
讓我們來(lái)寫(xiě)一個(gè)最簡(jiǎn)單的源生成器。首先創(chuàng)建一個(gè)新的類(lèi)庫(kù)項(xiàng)目:
<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>
然后寫(xiě)一個(gè)簡(jiǎn)單的生成器:
[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);
}
}
在消費(fèi)項(xiàng)目中引用這個(gè)生成器:
<ProjectReference Include="../HelloWorldGenerator/HelloWorldGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
現(xiàn)在你可以直接使用生成的代碼:
using Generated;
Console.WriteLine(HelloWorld.SayHello()); // 輸出: Hello from Source Generator!
進(jìn)階技巧:增量源生成器
上面的例子雖然能工作,但在大型項(xiàng)目中會(huì)有性能問(wèn)題。每次編譯都會(huì)重新生成所有代碼,就像每次做飯都要重新洗所有的鍋一樣浪費(fèi)。
這時(shí)候就要用到增量源生成器了。它采用了類(lèi)似React的虛擬DOM的思想,只有變化的部分才會(huì)重新生成:
[Generator]
public class SmartPropertyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 只關(guān)注有特定屬性的類(lèi)
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);
使用的時(shí)候只需要加個(gè)屬性:
[GenerateProperties]
public partial class Person
{
public string Name { get; set; }
// GeneratedProperty 會(huì)被自動(dòng)生成
}
實(shí)戰(zhàn)案例:FastService的妙用
說(shuō)到實(shí)際應(yīng)用,不得不提一下FastService這個(gè)項(xiàng)目。它用源生成器簡(jiǎn)化了ASP.NET Core的API開(kāi)發(fā),讓你寫(xiě)API就像寫(xiě)普通方法一樣簡(jiǎn)單。
傳統(tǒng)的Minimal API寫(xiě)法:
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("用戶(hù)管理")]
public class UserService : FastApi
{
[EndpointSummary("獲取用戶(hù)列表")]
public async Task<List<User>> GetUsersAsync()
{
return await GetAllUsersAsync();
}
[EndpointSummary("創(chuàng)建用戶(hù)")]
public async Task<User> CreateUserAsync(CreateUserRequest request)
{
return await SaveUserAsync(request);
}
}
源生成器會(huì)自動(dòng)分析方法名,推斷HTTP方法類(lèi)型:
Get*→ GET請(qǐng)求Create*,Add*,Post*→ POST請(qǐng)求Update*,Edit*,Put*→ PUT請(qǐng)求Delete*,Remove*→ DELETE請(qǐng)求
然后生成對(duì)應(yīng)的路由注冊(cè)代碼。這樣既保持了強(qiáng)類(lèi)型的優(yōu)勢(shì),又大大簡(jiǎn)化了代碼編寫(xiě)。
性能優(yōu)化的秘密武器
在開(kāi)發(fā)源生成器時(shí),有幾個(gè)性能優(yōu)化的小技巧:
1. 早期過(guò)濾
不要什么節(jié)點(diǎn)都分析,先用謂詞函數(shù)過(guò)濾掉不需要的:
var pipeline = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax cls &&
cls.AttributeLists.Count > 0, // 只看有屬性的類(lèi)
transform: static (ctx, _) => TransformNode(ctx))
2. 使用值類(lèi)型數(shù)據(jù)模型
千萬(wàn)不要在數(shù)據(jù)模型中保存Syntax或ISymbol對(duì)象,它們不能被正確緩存:
// ? 錯(cuò)誤做法
public record ClassInfo(ClassDeclarationSyntax Syntax, INamedTypeSymbol Symbol);
// ? 正確做法
public readonly record struct ClassInfo(
string Name,
string Namespace,
EquatableArray<PropertyInfo> Properties);
3. 對(duì)象池優(yōu)化
對(duì)于頻繁創(chuàng)建的對(duì)象,使用對(duì)象池:
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);
}
}
調(diào)試技巧:不再抓瞎
源生成器的調(diào)試曾經(jīng)是個(gè)大難題,但現(xiàn)在有了不少好用的技巧。
1. 斷點(diǎn)調(diào)試
在源生成器代碼中加入:
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch(); // 會(huì)彈出調(diào)試器選擇界面
}
#endif
}
2. 查看生成的代碼
在項(xiàng)目文件中加入:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
編譯后,生成的代碼會(huì)保存在Generated文件夾中,你可以直接查看。
3. 單元測(cè)試
寫(xiě)個(gè)測(cè)試來(lái)驗(yàn)證生成器的行為:
[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"));
}
常見(jiàn)陷阱與避坑指南
開(kāi)發(fā)源生成器時(shí),有幾個(gè)坑是新手經(jīng)常掉進(jìn)去的:
1. 命名空間沖突
生成的代碼可能和現(xiàn)有代碼沖突,記得加上合適的命名空間或者前綴:
// 生成的代碼加上特殊前綴
var className = $"Generated_{originalClassName}";
2. 編譯錯(cuò)誤處理
當(dāng)生成的代碼有語(yǔ)法錯(cuò)誤時(shí),編譯器的錯(cuò)誤信息可能不夠清晰。建議在生成器中添加診斷信息:
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. 增量生成緩存失效
如果數(shù)據(jù)模型設(shè)計(jì)不當(dāng),可能導(dǎo)致緩存頻繁失效:
// ? 這樣會(huì)導(dǎo)致緩存失效,因?yàn)镃ompilation對(duì)象每次都不同
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());
生態(tài)系統(tǒng)現(xiàn)狀
目前已經(jīng)有不少成熟的源生成器項(xiàng)目:
- System.Text.Json - 微軟官方的JSON序列化優(yōu)化
- Mapperly - 對(duì)象映射生成器
- FastService - API開(kāi)發(fā)簡(jiǎn)化
- StronglyTypedId - 強(qiáng)類(lèi)型ID生成
- Meziantou.Framework.StronglyTypedId - 另一個(gè)強(qiáng)類(lèi)型ID實(shí)現(xiàn)
這些項(xiàng)目都是學(xué)習(xí)源生成器的好例子,推薦大家去看看源碼。
總結(jié)
源生成器真的是一個(gè)很酷的技術(shù)。它不僅能大幅提升應(yīng)用性能,還能讓我們寫(xiě)出更簡(jiǎn)潔、更高效的代碼。雖然學(xué)習(xí)曲線有點(diǎn)陡峭,但一旦掌握了,你會(huì)發(fā)現(xiàn)很多以前覺(jué)得復(fù)雜的問(wèn)題都能用源生成器優(yōu)雅地解決。
如果你還在用傳統(tǒng)的反射做序列化、映射這些工作,不妨試試源生成器。相信我,一旦體驗(yàn)過(guò)那種編譯時(shí)生成代碼的快感,你就再也回不去了。
最后,學(xué)習(xí)新技術(shù)最好的方法就是動(dòng)手實(shí)踐。建議大家從簡(jiǎn)單的Hello World開(kāi)始,然后逐步嘗試更復(fù)雜的場(chǎng)景。記住,代碼是寫(xiě)給人看的,源生成器也不例外。寫(xiě)出清晰、可維護(hù)的生成器代碼,比寫(xiě)出復(fù)雜炫技的代碼更有價(jià)值。
Happy coding!
浙公網(wǎng)安備 33010602011771號(hào)