FUI框架反射注册迁移:Source Generator编译期装配实践
2026/9/15 4:50:16 网站建设 项目流程

上个月排查 FUI 冷启动卡顿,性能分析器把所有矛头都指向了启动阶段的反射注册:ScanAssemblies()在 52 个程序集上做GetTypes()加特性过滤,光这一项就吃掉 120 毫秒。当时第一反应是优化反射缓存,比如把扫描结果序列化落地,或者改用更轻的元数据读取方式。但越查越觉得不对——FUI 这套组件装配机制从第一天起就在用运行时发现的方式做事,类型之间的注册关系明明是编译期就确定的,却要每次启动都重新"考古"一遍程序集。于是才有了这篇文章,把 FUI 的装配从反射注册完整迁移到了 Source Generator 编译期装配。

这不是一篇只讲"怎么用 Source Generator"的入门教程,而是一次真实迁移的复盘:从哪个痛点切入、怎么设计生成代码的边界、增量生成器有哪些缓存上的坑、以及迁移前后的实测数据。如果你手头也有类似的自绘 UI 框架、依赖容器或插件系统,还在用反射做启动期装配,这篇文章应该能给你一条可以照抄的迁移路线。

1. 反射注册的痛:启动缓慢只是最轻的那一层

1.1 反射注册到底慢在哪里

FUI 早期的装配逻辑很典型:程序启动后遍历所有已加载程序集,过滤带[FUIComponent]特性的类型,然后逐个调用container.Register(...)。这段逻辑单独看单次执行并不算太慢,但问题是它每次冷启动都要完整执行一遍。

Assembly.GetTypes()的开销不在于"拿数组",而在于它会强制 CLR 解析程序集的完整元数据。如果你加载了 52 个程序集,其中有几个是几百个类型的大程序集,这一下就是全量元数据扫描。再加上自定义特性的匹配还需要读取每个类型的Attribute元数据,耗时是线性放大的。我们在 FUI 里实测是 120ms 起步,如果程序集数量翻倍,这个数字不是线性涨,而是接近指数上涨——因为每个程序集之间还有依赖关系的元数据解析。

一个很反直觉的现象:反射扫描慢,往往不是慢在你自己的业务类型上,而是慢在那些"为了扫描你不得不解析"的框架类型和基础类型上。程序集引用链越长,无关成本越高。

1.2 比性能更麻烦的三个隐性成本

时间久了你会发现,性能问题反而是最好解决的,真正磨人的是下面这三件事:

类型安全缺失。反射注册的代码往往长这样:Register(componentType, path),其中componentType是个Type对象。如果组件路径写错、注册类型拼错,编译期完全不会报错,只有跑到启动阶段才会炸。更隐蔽的是,有些人图省事会直接把某个基类或接口注册进去,运行时才暴露出"这个组件根本不能被实例化"的问题。

依赖关系成了黑盒。反射注册意味着没有显式的调用点。你想知道MainPage在哪里被注册?只能在启动代码里打断点或者加日志。IDE 的"查找所有引用"根本帮不上忙,全局搜索MainPage只能搜到定义处和 XAML 引用处。重构组件名的时候,纯粹靠人肉确认装配关系,漏一个就是线上事故。

AOT/裁剪环境直接傻眼。近两年 .NET 的 NativeAOT 和 iOS 裁剪越来越普及,反射注册在这种环境下是最难受的。因为裁剪器看不到反射调用关系,会把程序集里"看起来没用"的类型整个剪掉,然后你运行时GetTypes()拿不到类型。要解决就得维护一大坨DynamicDependency和裁剪配置文件,比反射代码本身还难维护。

我整理了一张对比表,看完基本就能理解为什么要动这个手术了:

维度反射注册Source Generator 编译期装配
启动注册耗时毫秒级,随程序集数量膨胀近似零,生成代码直接执行
类型安全运行期才暴露编译期生成,逻辑写在生成代码里
依赖可见性黑盒,无调用点显式注册代码,IDE 可跳转
AOT/裁剪友好度差,需额外配置文件好,静态类型引用
重构影响容易漏改编译器直接报错或自动跟随
调试体验断点难打,启动链路长生成代码可读,断点清晰

2. 迁移前的总体规划:把"运行时发现"改写成"编译期断言"

2.1 先盘点哪些装配信息是编译期可以确定的

迁移之前,我花了整整一天时间把 FUI 的装配逻辑全部摊开,分类标记每一行代码的"信息可得时间"。你会发现一个有意思的事实:绝大多数装配信息在编译期就已经是确定的了。

FUI 里需要注册的东西大概分四类:

  • [FUIComponent]的页面/组件类型:类型名、命名空间、继承关系,编译期全确定。
  • 组件路径和生命周期策略:这些是特性构造函数里的常量,编译期也确定。
  • 组件之间的导航关系:如果导航目标是通过typeof(SomePage)硬编码的,编译期确定;如果是字符串路由,那要单独做路由表。
  • 启动时所需的初始化顺序:这部分往往依赖外部配置,不能全塞进编译期。

我们的结论是:前两类完全可以交给编译期生成,第三类可以先做静态登记再保留运行时查表,第四类保持现状。这个"分类法"比一上来就闷头写 Generator 重要得多。它的本质是重新审视你的装配逻辑:哪些是"事实",哪些是"策略"。事实交给编译期,策略留给运行时。

2.2 生成代码还是生成数据:两种产物的取舍

确定了迁移范围后,紧接着要回答一个设计问题:Source Generator 到底应该生成什么?是生成一堆直接执行的注册代码,还是生成一个可以被运行时消费的数据表?

生成代码的特点是显式、可读、可以利用编译器的静态检查。比如直接生成:

container.Register<MainPage>("/main", ComponentLifetime.Singleton);

这种写法最大的好处是:如果MainPage不存在了,编译直接报错;如果你改了构造函数签名,编译器立刻告诉你。但缺点是生成代码和你的容器 API 耦合了——容器将来改接口,生成器也得跟着改。

生成数据的特点是通用、稳定。你可以生成一个静态数组:

internal static readonly ComponentRegistration[] Registrations = new[] { new ComponentRegistration(typeof(MainPage), "/main", ComponentLifetime.Singleton), };

运行时由框架统一遍历这个数组做注册。好处是生成逻辑简单,不会因为容器接口调整就崩;坏处是typeof(MainPage)的引用丢失后,编译期不会立刻报警——虽然比反射强,但还没有把静态检查的红利吃到极致。

FUI 最终选择了生成代码方案。原因有三个:一是我们的容器接口相当稳定,短期内不会改;二是我们希望在迁移期间尽可能早地暴露装配错误;三是生成代码可以直接在调试时单步进去看,团队协作时的认知成本最低。如果你更看重框架和生成器解耦,生成数据也完全可行。关键是这条路你在动手前就要想清楚,不然后面返工很痛苦。

2.3 为什么用增量生成器而不是传统 ISourceGenerator

Roslyn 里有两代 Source Generator 接口:老的ISourceGenerator和新的IIncrementalGenerator。只做一次性 Demo 的话两者看不出差别,但放进真实项目里差别非常大。

老生成器每次编译都会对整个编译对象重新执行一遍生成逻辑,哪怕你只改了一个文件里的一个字符。这意味着在 IDE 里每敲一次键盘,生成器就要全量跑一次。程序集一多,Visual Studio 直接卡成幻灯片。

增量生成器通过IncrementalValueProvider做到了缓存复用。语法树、语义模型、特性信息都可以被拆成独立的"数据流水线",只有上游发生变化的下游才会重新计算。所以虽然第一次全量构建还是慢,但增量编译时几乎没有任何感知。

这也是为什么现在新写的 Generator 基本都默认IIncrementalGenerator。后面实现的全部代码都基于这个接口。

3. 手写 Source Generator 的完整流程:从标记特性到自动装配

3.1 定义 FUIComponentAttribute 与运行库侧接口

迁移的第一步不是写 Generator,而是先把"标记"这件事规范化。我们定义了FUIComponentAttribute,放在 FUI.Abstractions 程序集里:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class FUIComponentAttribute : Attribute { public string? Path { get; set; } public ComponentLifetime Lifetime { get; set; } = ComponentLifetime.Transient; }

注意几个细节:Inherited = false很关键,否则基类带特性会导致所有派生类都被扫描到,这在实际项目里会造成重复注册。AllowMultiple = false也是常识,一个类型的注册语义不应该有歧义。

与此同时,我定义了生成代码要对接的容器接口,保持最基本的样子:

public interface IFUIContainer { void Register<TComponent>(string? path = null, ComponentLifetime lifetime = ComponentLifetime.Transient); }

这里有一个早期的反模式:有人在FUIComponentAttribute里塞了Type类型的参数,比如[FUIComponent(typeof(IMainPage))]。能跑,但这会让特性参数失去常量语义,生成器读取时也麻烦。能用字符串路径和枚举解决的问题,不要在特性里塞复杂类型。

3.2 创建 Generator 项目并配置工程引用

Generator 项目本身必须目标netstandard2.0,因为 Roslyn 要在各种 .NET 环境下加载它(包括 .NET Framework 的 MSBuild 进程)。代码里不能随便用最新 C# 语法,至少类库层面要保守。项目文件长这样:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <Nullable>enable</Nullable> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" /> </ItemGroup> </Project>

EnforceExtendedAnalyzerRules这个属性建议加上,它会启用针对 Analyzer/Generator 的额外代码分析规则,提前暴露一些隐患。Microsoft.CodeAnalysis.CSharp的版本也不要追新——你的 Generator 要在目标机器上的 Roslyn 版本范围里跑,太新的包版本可能无法被老编译器加载。

然后在 FUI 宿主项目里引用这个 Generator:

<ItemGroup> <ProjectReference Include="..\FUI.Generator\FUI.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" /> </ItemGroup>

关键点:

  • OutputItemType="Analyzer":把项目引用作为分析器加载,而不是程序集引用。
  • ReferenceOutputAssembly="false":不要给宿主项目暴露 Generator 里的类型。
  • PrivateAssets="all":防止这个 Analyzer 引用被传递到下游项目。

3.3 核心生成逻辑:ForAttributeWithMetadataName + Collect + RegisterSourceOutput

直接上最核心的 Generator 实现。以下代码就是 FUI 编译期装配的骨架:

using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace FUI.Generator; [Generator(LanguageNames.CSharp)] public sealed class FUIComponentIncrementalGenerator : IIncrementalGenerator { private const string AttributeFullName = "FUI.Abstractions.FUIComponentAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { var componentInfos = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeFullName, static (node, _) => node is ClassDeclarationSyntax, static (ctx, _) => GetComponentInfo(ctx)) .Where(static info => info is not null) .Select(static (info, _) => info!); context.RegisterSourceOutput(componentInfos.Collect(), static (spc, infos) => { var source = BuildSource(infos); spc.AddSource("FUIComponentRegistration.g.cs", SourceText.From(source, Encoding.UTF8)); }); } private static ComponentInfo? GetComponentInfo(GeneratorAttributeSyntaxContext ctx) { if (ctx.TargetSymbol is not INamedTypeSymbol symbol) return null; var fullName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var path = "/" + symbol.Name.ToLowerInvariant(); foreach (var attribute in symbol.GetAttributes()) { if (attribute.AttributeClass?.ToDisplayString() != AttributeFullName) continue; foreach (var namedArg in attribute.NamedArguments) { if (namedArg.Key == "Path") path = namedArg.Value.Value?.ToString() ?? path; } } return new ComponentInfo(fullName, path); } private static string BuildSource(IReadOnlyList<ComponentInfo> infos) { var builder = new StringBuilder(); builder.AppendLine("// <auto-generated/>"); builder.AppendLine("#nullable enable"); builder.AppendLine("namespace FUI.Generated"); builder.AppendLine("{"); builder.AppendLine(" internal static class FUIComponentRegistration"); builder.AppendLine(" {"); builder.AppendLine(" public static void RegisterAll(global::FUI.Abstractions.IFUIContainer container)"); builder.AppendLine(" {"); foreach (var info in infos) { builder.AppendLine($" container.Register<{info.FullName}>(\"{info.Path}\");"); } builder.AppendLine(" }"); builder.AppendLine(" }"); builder.AppendLine("}"); return builder.ToString(); } } internal sealed record ComponentInfo(string FullName, string Path);

这段代码的核心在ForAttributeWithMetadataName。这个 API 是 Roslyn 4.3.1 之后引入的,它做的事情是:在编译中查找所有标记了指定全名特性的语法节点,并直接提供语义模型信息。

这里必须单独强调一个点:不要自己在SyntaxProvider.CreateSyntaxProvider里手写"找特性"的逻辑。早期我见过很多生成器代码,先从ClassDeclarationSyntax里找AttributeListSyntax,再去解析AttributeSyntax的名称,最后用SemanticModel.GetSymbolInfo拿语义符号。这套做法在老代码里很常见,但性能差且容易踩语法层面的坑(比如global::FUI.Abstractions.FUIComponent这种写法就会让字符串匹配翻车)。ForAttributeWithMetadataName直接从语义层匹配符号,干净利落。

3.4 生成代码的文件与可读性设计

生成文件的hintName我固定用了"FUIComponentRegistration.g.cs",没有按类型拆分。理由很实际:FUI 的组件数量在几千量级以内,一个文件生成 2000 行注册代码完全可接受,生成一个文件还能减少编译器的文件处理开销。

可读性方面有几个原则:

  • 文件头必须带// <auto-generated/>,告诉工具链和同事这个文件不要手改。
  • 生成代码的缩进要规范,不要为了省字符串拼接把代码全怼成一行。
  • 类型名用global::前缀,避免命名空间冲突。
  • #nullable enable要带上,防止消费方项目因为可空上下文不一致报警。

我还额外做了一个增强:生成器在发现两个组件的 Path 相同时,会输出一个编译诊断FUI001,直接编译报错。这个在反射时代是完全做不到的,因为运行时才能遍历出完整注册表。写法和普通诊断一样:

var duplicatedPaths = infos.GroupBy(i => i.Path).Where(g => g.Count() > 1); foreach (var group in duplicatedPaths) { spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor("FUI001", "Duplicate component path", "The component path '{0}' is registered more than once.", "FUI", DiagnosticSeverity.Error, true), location: null, group.Key)); }

3.5 在宿主入口接入自动装配

最后一步是修改 FUI 的启动代码,把原来的反射扫描换成一行生成代码调用:

var container = new FUIContainer(); FUI.Generated.FUIComponentRegistration.RegisterAll(container);

原先 50 行ScanAssembliesFilterTypesGetCustomAttributesTryRegister的链路,直接从启动路径上消失了。剩下的就是把RegisterAll方法体里的注册记录当作"装配清单"——FUI 的组件关系从此跃然纸上。

如果你有多个入口(比如不同的宿主进程),记得每个入口都要调用RegisterAll。或者更省事,在FUIContainer的构造函数里直接调用,这样任何入口创建容器都会自动装配。FUI 最终选的是后者,因为团队里确实有人会忘。

4. 增量生成器的两个大坑:缓存失效与配置项读取

4.1 为什么不能在 Generator 里静态缓存注册表

迁移过程中的第一个大坑来自于一个写惯了普通库代码的直觉:把收集到的组件信息缓存到静态字典里,避免重复计算。

但增量生成器有一条铁律:所有跨编译会话的状态都必须放在语法树和语义模型推导出的 value provider 里,任何静态容器都不要碰。我之前在调试一个重复注册问题的时候,发现自己写了一个static ConcurrentDictionary<string, ComponentInfo>来缓存已收集的信息。结果就是:第一次构建正常,第二次改了个组件路径再构建,生成代码里依然保留旧路径。因为静态字典在整个 MSBuild 进程生命周期里是常驻的,增量编译时 provider 会复用上次的结果,但我自己又往静态字典里塞了新的,两边不一致,生成代码就处于"薛定谔状态"。

正确的做法是:让你的 transform 函数保持纯函数。输入是语法节点或符号,输出是ComponentInfo,不要依赖任何外部可变状态。增量生成器自己会做缓存和失效判断,你只需要把每个类型的信息算干净。

4.2 Collect 的增量损失与取舍

第二个坑是我自己选的,但值得说清楚。上面的实现里用了.Collect(),这个操作会把所有ComponentInfo汇总成一个ImmutableArray再统一生成一个源文件。好处是代码简单,坏处是:任何一个组件变更都会导致整个RegisterAll方法重新生成,增量的粒度被放到了"所有组件"级别。

FUI 项目目前的规模下,全量重生成也只需要几毫秒,完全可接受。但如果组件数量上到几十万(比如编辑器类软件的扩展点系统),你就必须考虑拆文件:每个类型生成一个独立的注册方法,再生成一个汇总调用的骨架。那种方案的增量粒度小,但生成代码的复杂度会上一个台阶。

我的建议很简单:先按规模选方案,不要为了"更增量"而把复杂度堆高。规模大了再拆,有实测数据支撑的迁移才是理性的。

4.3 调试技巧:从 Debugger.Launch 到最小复现工程

Generator 卡死、生成代码不对、诊断不触发,这些调试起来很痛苦,因为 Generator 是在编译进程里跑的,不会跟你的 IDE 调试会话自动挂钩。

第一个入门技巧是Debugger.Launch()

#if DEBUG System.Diagnostics.Debugger.Launch(); #endif

把这行放在Initialize或某个 transform 的开头。编译时它会弹出一个"选择调试器"的对话框,你选当前 Visual Studio 实例就能命中断点。注意本地调试分支要用#if DEBUG包住,否则 CI 上的编译也会卡在断点提示上。

第二个技巧是我强烈推荐的:建一个极小的复现工程。比如在tests/FUI.Generator.Tests里只放一个MainPage类和特性的最小定义,用 Roslyn 的CSharpGeneratorDriverAPI 直接跑生成器:

var compilation = CreateCompilation(source); GeneratorDriver driver = CSharpGeneratorDriver.Create(new FUIComponentIncrementalGenerator()); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); var runResult = driver.GetRunResult();

这样不用启动整个 FUI 宿主应用,几秒钟就能验证生成器逻辑是否正确。等到生成代码符合预期,再回到大项目里做集成验证。

4.4 提示名称冲突与多目标框架下的生成文件问题

还有一个容易忽略的点:一个项目如果同时TargetFrameworks多个 TFM,比如net8.0net48,生成器的AddSource会在每个 TFM 的编译里各执行一次,如果hintName相同,不同 TFM 间不会冲突,但同一个编译里如果多个生成器都产生同名文件就会报错。

为了避免这种问题,如果将来生成的文件不只是一个,建议在hintName里加入稳定的哈希后缀。比如把所有组件类型完整名拼起来算一个 SHA256,取前 8 位:

var hash = string.Concat(infos.Select(i => i.FullName + ";")).ComputeSha256().Substring(0, 8); spc.AddSource($"FUIComponentRegistration.{hash}.g.cs", ...);

这样生成文件名的可预测性差一点,但基本杜绝了冲突。FUI 目前只有一个文件,我保留的是固定文件名,主要是为了 Git 差异对比时容易判断生成内容的变化。

5. 迁移前后实测:启动耗时、调试体验与交付规范的变化

5.1 启动数据对比:一次彻底的性能释放

迁移完成后,我在同样的测试机上跑了三次冷启动,数据如下(环境:Windows 11,.NET 8,Release 配置,RyuJIT):

指标反射注册Source Generator 编译期装配
注册阶段耗时123ms0.3ms
冷启动完成时间412ms268ms
启动期程序集元数据解析数52 个仅加载实际用到的
启动期峰值内存增量48MB20MB

注册阶段从 123ms 降到 0.3ms,这个幅度其实不意外,因为生成代码就是一组直接的方法调用,连循环都没有。整体冷启动缩减了 144ms,对桌面端来说体感非常明显,尤其是在老机器上。

但我要泼一盆冷水:如果你的系统本身启动已经很快,比如反射注册只花 5ms,这个迁移带来的"性能红利"可能远小于你的改造投入。这时候迁移的真正价值不在性能,而在第二节和第五节的这些维护性收益。不要纯粹为了炫技把一个 10ms 的反射改成 Generator,性价比不划算。

5.2 调试体验与合作协作的变化

数据之外,有个我没有预料的收益是团队协作层面的。反射时代,新人入职看 FUI 装配逻辑,基本是一脸懵:启动代码里就一行RegisterAllComponents(),根本不知道哪些类型被注册了,只能跑起来后看日志。现在直接打开FUIComponentRegistration.g.cs,所有组件、路径一目了然。

代码评审也轻松了很多。以前合并请求里如果改了某个组件的路径,评审人根本没法判断这个改动会不会影响启动装配。现在路径在特性上写着,而且生成器在编译期就会因为重复路径直接报错,评审人的注意力可以完全集中在业务逻辑上。

排查问题更是这样。反射时代,某个组件注册失败了,你得在启动链路里加日志、看异常堆栈,定位到具体是哪个类型哪一步炸的。现在生成代码就是一组静态调用,IDE 单步进去,谁先谁后、哪个路径传错了,全摆在明面上。

5.3 生成器诊断规则带来的质量前移

这轮迁移让我对"编译期质量前移"有了新的理解。过去很多"运行时才能发现的问题",其实都是因为类型关系没有被静态化。编译器不是不能查,是你压根没告诉它哪些信息是重要的。生成器天然适合在这层做"断言"。

目前 FUI 的 Generator 里除了重复路径检查,还加了两条诊断规则:

  • FUI002FUIComponentAttribute标记在非partial类上时给提示(虽然生成方案不依赖 partial,但框架规范建议组件类都声明为 partial,便于未来做代码注入扩展)。
  • FUI003Path/开头但包含空格时给警告(这种路径在导航解析里会产生歧义)。

这些规则放在 Generator 里有一个天然优势:它能在语义模型层面做事,比单纯的正则表达式匹配可靠得多。而且规则和生成过程共享同一份ComponentInfo数据,不需要写第二套解析逻辑。

这套思路也延伸到了新组件开发流程里:新同事写一个页面,加上特性,编译一过,组件就自动进入了装配清单,不需要记得去某个启动文件里改注册代码。从"记得注册"变成了"声明即注册"。这体验上的差别,用过的团队都回不去了。

6. 后续可以继续扩展的三个方向

迁移做完之后,我又往前多想了几步。这些不算 FUI 线上必须做的,但对想要进一步榨干编译期装配价值的人来说,是很好的延展方向。

方向一:把装配清单导出为静态路由表。既然 Generator 已经收集了所有组件的 Path 和类型映射,那就没必要只在内存里用一次。可以顺手生成一个 JSON 或 CSV 文件,交给路由工具、文档系统甚至自动化测试用例消费。注意这需要 Generator 不但AddSource,还要通过AdditionalText或者EmitCompilerGeneratedFiles的方式把非源码产物输出到项目目录。

方向二:把组件生命周期校验提前。比如某个组件标记了Singleton,但构造函数依赖另一个Transient组件,这在反射时代是运行期才会暴露的问题。生成器可以读取构造函数参数,配合一个简单的依赖规则分析器,直接在编译期给出诊断。这就是把一套迷你依赖分析器搬到了编译期。

方向三:与热重载/热更新机制打通。编译期生成的注册表是静态的,但业务系统往往希望某些组件可以动态替换。一个可行的折中是:生成器把所有组件注册到一个静态字典,运行时热更新只需要替换字典里的映射关系,不需要重新生成代码。FUI 目前还没有走到这一步,但架构上已经留好了口子。

最后分享一个实用的经验。迁移到编译期装配之后,我踩到最多的问题反而不是 Generator 本身的写法,而是引用关系:ProjectReference里漏了OutputItemType="Analyzer"、或者把 Generator 项目直接ProjectReference进了发布项目。建议迁移初期先在独立的分支上做,专门用一个几行的 Demo 宿主验证引用关系,确认生成代码能正常产出,再合并回主线。生成器这种东西,一旦引用关系错了,症状是"没有任何效果"而不是"编译报错",排查起来非常反直觉。

整个实践下来,我的体会是:编译期装配的本质不是简单地把反射逻辑翻译成生成代码,而是把"类型关系"从运行时重新提升为编译期可见的静态事实。对于任何有装配需求的框架——不管是 UI 框架、依赖注入容器还是插件系统——只要类型关系在写代码时已经确定,Source Generator 就值得一试。先小范围验证,再逐步扩大覆盖面,你会看到一个更快速、更安全、也更可阅读的系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询