尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从反射到编译期:FUI组件装配的Source Generator重构之路

发布时间:2026/9/14 12:41:09

资讯中心
01
ARTICLE

从反射到编译期:FUI组件装配的Source Generator重构之路

从反射到编译期:FUI组件装配的Source Generator重构之路
1. 从反射注册到编译期装配FUI 项目的一次底层重构做 FUI 这套框架时我最头疼的不是组件怎么写而是组件怎么被框架发现。早期版本用的是很经典的反射注册方案程序集启动时扫描所有类型找到带特定标记的类再通过Activator.CreateInstance实例化并塞进容器。这套方案在组件量少的时候没问题但等 FUI 的组件库膨胀到几百个后问题一个接一个浮出水面。启动变慢、错误要运行到那行代码才炸出来、IDE 里点不到实现……最难受的是明明编译器能发现的问题非得等用户跑到某个页面才崩。这次实践的核心目标是把 FUI 的装配时机从运行时往前挪到编译期。具体手段就是 Source Generator借助 Roslyn 在编译阶段扫描代码、生成注册逻辑把原来反射那一套整段替换掉。做完之后最直观的变化是组件装配错误在编译时就会以异常的形式直接卡住构建根本走不到运行那一步。这篇文章适合正在维护 UI 组件库、依赖注入容器或插件化框架的开发者阅读。哪怕你不是做 C#只要你的技术栈有反射注册这类运行时装配的痛点这个改造思路也能给你一些参考。2. FUI 装配机制为什么要动刀子2.1 反射注册到底卡在哪儿先还原一下 FUI 早期版本的典型注册代码。框架里定义了一个特性组件类打上标记启动时统一扫描[AttributeUsage(AttributeTargets.Class)] public sealed class FUIComponentAttribute : Attribute { public string? Route { get; set; } }装配阶段用反射遍历程序集var assembly typeof(App).Assembly; var types assembly.GetTypes(); foreach (var type in types) { var attr type.GetCustomAttributeFUIComponentAttribute(); if (attr null) continue; var instance Activator.CreateInstance(type) as IFUIComponent; ComponentRegistry.Register(attr.Route ?? type.Name, instance); }这段代码表面看没毛病实际上到处是雷。第一assembly.GetTypes()会触发整个程序集所有类型的元数据加载。FUI 的组件项目引了十几个库启动时这个调用要额外加载几百 MB 的元数据实测在低端设备上能多出 800 毫秒到 2 秒的启动延迟。对工具类 App 来说还能忍对追求秒开的场景就不行了。第二错误出现得太晚。如果某个组件类忘了写无参构造函数Activator.CreateInstance运行到那一行直接抛MissingMethodException。如果两个组件注册了同一个路由不会报错只是后注册的把先注册的覆盖掉。这种问题在开发环境可能很久都暴露不出来直到某个用户点进特定页面才崩。第三IDE 支持几乎为零。typeof(ComponentRegistry)只看得到字符串路由跳不到具体实现类重构时改类名、改路由都是靠全局搜索硬找非常容易漏。第四容器里存的全是接口或基类引用实际是什么实现类完全由反射决定。真出了装配问题排查链路是从运行时异常往回翻代码翻半天找到注册逻辑还得再找是哪一步实例化失败。2.2 “编译期异常”才是真正的护城河微博技术圈最近流行一个热词叫“编译期异常”说的是把运行时会暴露的错误尽量提前到编译阶段让编译器帮你兜底。这个思路用在 FUI 装配上价值立竿见影。编译期异常不是指 CLR 在运行时报的Exception而是指 MSBuild/Roslyn 在编译过程中上报的Diagnostic。它的错误等级可以是Error一旦出现整个dotnet build直接失败根本产不出程序集。这就意味着组件没注册上、路由冲突、类型不满足约束这些问题在 CI 阶段就挡住而不是发版后让用户遇到。我见过太多因为反射注册把问题拖到生产环境的案例这真的是最贵的 bug 种类。2.3 Source Generator 能改变什么Source Generator源生成器是 Roslyn 提供的一种编译时钩子。它在编译器完成语法和语义分析后、生成 IL 之前执行可以读取项目里所有代码的语法树和语义模型然后直接生成额外的 C# 源代码参与编译。FUI 的改造思路就是这样用 Source Generator 在编译期扫描所有带FUIComponentAttribute的类直接生成一段注册代码把instance的创建和注册逻辑原样写死。运行时不清扫任何类型不做任何反射调用所有信息在编译时就已经确定。效果是启动路径上没有反射性能显著提升类型关系在编译时就校验完毕错误直接变成编译期异常IDE 可以完整跳转重构能吃到 Roslyn 的全量分析生成的代码可读、可调试汇编层面也能看出来实例化点在哪里当然Source Generator 不是银弹。它有自己的学习曲线踩坑点也不少我下面会把整个实现路径和坑位都摊开来讲。3. FUI 编译期装配的完整设计与落地方案3.1 总体设计目标明确边界清晰动手之前我先列了一份约束清单确定这次改造的边界FUI 框架对外 API 保持兼容业务方不需要改动组件代码注册信息必须在编译期全部确定运行时不做任何程序集扫描同一路由重复注册要变成编译期错误而不是运行期静默覆盖生成的代码必须可读出问题能直接定位到具体类型开发体验不能退化普通类库项目、Web 项目、AOT 发布场景都要能跑带着这五条约束方案选型就很清楚了。反射注册死活得换掉能选择的替代方案有三个手写注册代码、Emit 动态生成、Source Generator 静态生成。手写注册代码最直接但 FUI 组件库里类型太多手工维护不现实新增一个组件就忘一个注册最终会依赖某个运行时检查来兜底本质上还是回到了反射那套。Emit 动态生成性能好但调试难度极高DynamicMethod生成 IL 一旦出错堆栈信息完全不可读。Source Generator 是三者里平衡度最好的编译期生成、运行时空调用、生成的 C# 代码可直接阅读。3.2 定义装配契约特性与生成器接口FUI 的装配单元不止组件一种还有主题皮肤、消息处理器、自定义元素解析器。所以我定义了一个统一的FUIComponentAttribute但注册目标用参数区分public enum FUIRegistrationKind { Component, // 页面组件 Skin, // 主题皮肤 Handler, // 消息处理器 Parser // 自定义元素解析器 } [AttributeUsage(AttributeTargets.Class, AllowMultiple false)] public sealed class FUIComponentAttribute : Attribute { public FUIRegistrationKind Kind { get; } public string? Key { get; set; } public int Priority { get; set; } public FUIComponentAttribute(FUIRegistrationKind kind) { Kind kind; } }业务方用法不变还是打在类上。例如一个页面组件[FUIComponent(FUIRegistrationKind.Component, Key login)] public sealed class LoginView : IFUIComponent { public void Initialize() { } }一个主题皮肤[FUIComponent(FUIRegistrationKind.Skin, Key midnight)] public sealed class MidnightTheme : IFUISkin { public string Name midnight; }重点在于所有实现类型都要实现对应的标记接口。这不是我多此一举而是为了让生成器有强类型检查的抓手。生成器在编译期看到带特性的类时可以检查这个类是否实现了对应接口没有就直接报一个编译期错误比运行时才发现类少实现了一个方法强太多了。3.3 搭建 Incremental Generator 骨架这里我用的是IIncrementalGenerator不是老式的ISourceGenerator。两者的区别在于增量缓存IIncrementalGenerator可以按依赖拆分 Pipeline只对发生变化的输入重新计算在大型项目里编译速度差异非常明显。生成器的核心骨架如下using System.Collections.Immutable; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace FUI.Build; [Generator(LanguageNames.CSharp)] public sealed class FUIComponentGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var candidates context.SyntaxProvider .ForAttributeWithMetadataName( FUI.FUIComponentAttribute, static (node, _) node is ClassDeclarationSyntax, static (ctx, _) GetComponentTarget(ctx)) .Where(static target target is not null) .Select(static (target, _) target!); var combined candidates.Collect(); context.RegisterSourceOutput(combined, static (spc, targets) { var writer new CodeWriter(); writer.WriteHeader(); writer.WriteUsings(); foreach (var target in targets) { writer.WriteRegistration(target); } var source writer.ToString(); spc.AddSource(FUI.Generated.Registrations.g.cs, SourceText.From(source, Encoding.UTF8)); }); context.RegisterPostInitializationOutput(static ctx { ctx.AddSource(FUI.Generated.CompileTimeGuard.g.cs, SourceText.From(Stubs, Encoding.UTF8)); }); } }有几个细节值得讲清楚。ForAttributeWithMetadataName是 Roslyn 4.3 提供的高性能 API。它的实现原理是直接走SymbolInfo查特性符号不需要手动过滤所有语法节点再解析语义模型。过渡写法通常是用SyntaxProvider.CreateSyntaxProvider先扫语法树再GetSemanticModel去查特性这样在大项目里语法树扫描和语义解析是纯性能浪费。我实测过同样的项目用ForAttributeWithMetadataName比老写法快 3 倍左右。GetComponentTarget方法封装了从语义模型提取类型信息的逻辑private static ComponentTarget? GetComponentTarget(GeneratorAttributeSyntaxContext context, CancellationToken token) { var symbol context.TargetSymbol as INamedTypeSymbol; if (symbol is null || symbol.IsAbstract || symbol.TypeKind ! TypeKind.Class) { return null; } var attr context.Attributes[0]; var kind (FUIRegistrationKind)attr.ConstructorArguments[0].Value!; var key attr.NamedArguments.FirstOrDefault(static n n.Key Key).Value.Value as string; var priority attr.NamedArguments.FirstOrDefault(static n n.Key Priority).Value.Value is int p ? p : 0; var interfaceName kind switch { FUIRegistrationKind.Component IFUIComponent, FUIRegistrationKind.Skin IFUISkin, FUIRegistrationKind.Handler IFUIMessageHandler, FUIRegistrationKind.Parser IFUICustomParser, _ null }; var valid symbol.AllInterfaces.Any(static (i, name) i.ToDisplayString() name, interfaceName); if (!valid) { return new ComponentTarget { FullyQualifiedType symbol.ToDisplayString(), Kind kind, Key key, Priority priority, HasInterfaceError true }; } return new ComponentTarget { FullyQualifiedType symbol.ToDisplayString(), Kind kind, Key key, Priority priority, HasInterfaceError false }; }这里有个关键点context.Attributes[0]拿到的是编译期语义模型里的特性数据不是反射元数据。所以ConstructorArguments里的值已经解析好了不需要再做类型转换的糙活。但要注意NamedArguments的顺序不保证和声明顺序一致所以要用FirstOrDefault按名字取。3.4 编译期错误把“坏味道”拦在 build 阶段前面说的接口缺失检查落实成编译期异常就需要往SourceProductionContext里报Diagnostic。我把注册发现阶段和代码生成阶段分开处理private static void CheckTargets(SourceProductionContext context, ImmutableArrayComponentTarget targets) { var seenKeys new Dictionarystring, string(StringComparer.Ordinal); foreach (var target in targets) { if (target.HasInterfaceError) { context.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( FUI1001, 组件未实现指定接口, ${target.FullyQualifiedType} 标记了 FUIComponent 特性但没有实现对应的接口, FUI.CompileTime, DiagnosticSeverity.Error, true), Location.None)); continue; } var key ${target.Kind}:{target.Key}; if (!string.IsNullOrEmpty(target.Key) !seenKeys.TryAdd(key, target.FullyQualifiedType)) { context.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( FUI1002, 统一路由重复注册, $路由 {key} 已经被 {seenKeys[key]} 注册{target.FullyQualifiedType} 无法再次注册, FUI.CompileTime, DiagnosticSeverity.Error, true), Location.None)); } } }FUI1001和FUI1002就是 FUI 工程里的“编译期异常”。它们走的是 Roslyn 的诊断管道在错误列表里直接显示会阻断dotnet build而且提示信息是开发人员能直接看懂的省了运行时翻堆栈的时间。为了让错误定位到具体代码行我建议在循环里尽量把Location传进去。上面示例用的Location.None只是示意实际项目中应该用context.TargetNode.GetLocation()拿到具体语法树位置。3.5 生成注册代码的正确姿势生成代码时要注意一个 FUI 特有的问题组件之间的依赖关系。有些组件初始化时依赖其他组件比如LoginView依赖主题服务。我采用延迟初始化加优先级排序的方式internal sealed record ComponentTarget { public required string FullyQualifiedType { get; init; } public required FUIRegistrationKind Kind { get; init; } public string? Key { get; init; } public int Priority { get; init; } public bool HasInterfaceError { get; init; } }生成的注册代码结构上固定为internal static partial class FUIRegistrations { public static void RegisterAll(IFUIRegistry registry) { // 注册消息处理器按优先级升序 registry.RegisterHandlerLoginMessageHandler(default); registry.RegisterHandlerLogoutMessageHandler(default); // 注册主题皮肤 registry.RegisterSkinMidnightTheme(); // 注册页面组件 registry.RegisterComponentLoginView(login); } }这段代码并不复杂关键是怎么在生成器里高效地拼字符串。我不建议用$直接拼因为组件数量一多字符串插值的转义和你需要维护的引号会让人崩溃。我建议写一个CodeWriter辅助类专门做缩进管理和代码块拼接internal sealed class CodeWriter { private readonly StringBuilder _sb new(); private int _indent; public void WriteLine(string line ) { if (line.Length 0) { _sb.AppendLine(); return; } for (var i 0; i _indent; i) { _sb.Append( ); } _sb.AppendLine(line); } public void OpenBlock() { WriteLine({); _indent; } public void CloseBlock() { _indent--; WriteLine(}); } public override string ToString() _sb.ToString(); }这样生成的代码缩进干净、可读性好。使用者打开obj目录下生成的.g.cs文件就能看到一段规整的注册逻辑。3.6 关键取舍为什么不用直接实例化而是交给注册器有朋友可能会问既然生成器已经把类型确定下来了为什么不直接new LoginView()然后registry.RegisterComponent(instance)非要传泛型类型参数这里涉及到 FUI 组件的生命周期管理。FUI 的组件有的是单例有的每个页面创建新实例还有的按需要做依赖注入。如果生成器直接实例化生命周期策略就只能在注册代码里写死后续想改成按需创建就得重新生成代码太死板。所以我把“创建实例”这一步留给IFUIRegistry的实现去处理生成器只负责把类型元数据告诉注册器。RegisterComponentT的泛型参数带上类型约束registry.RegisterComponentLoginView(login);这一个设计让整个系统保持弹性注册器内部可以按 Key 做单例缓存可以延迟创建对象可以处理构造函数注入。而这一切对生成器来说完全透明。4. 生成器背后的工程化细节与坑位排查4.1 增量缓存的正确姿势IIncrementalGenerator的核心优势在“增量”二字。如果 Pipeline 里的某个步骤把不可比较类型放进缓存增量机制就会失效导致每次编译都全量重跑。最容易犯的错是把SemanticModel当成返回值存下来。SemanticModel不是值类型没法做结构化缓存比较用一次全量编译后第二次编译大概率不会命中缓存。正确做法是只提取出需要的标量数据比如类的 fully qualified name、Key、Priority等把它们放进record或struct里返回。另外Collect()拿到的是ImmutableArrayComponentTarget如果组件集合很大每次对数组的排序操作也要放到缓存内完成。我在生成器里对targets做了按Kind和Priority排序这个排序结果存回管线后续编译如果集合顺序没变缓存直接命中。4.2 生成器没有生效的排查清单刚接入 Source Generator 时最常见的现象是生成器没跑但也没报错。排查顺序按下面这张表来现象可能原因排查方式生成器完全没有输出项目没有引用Microsoft.CodeAnalysis.CSharp或生成器程序集版本不匹配检查.csproj里的ProjectReference是否带OutputItemTypeAnalyzer ReferenceOutputAssemblyfalseVS 错误列表没有 FUI1001项目启用了EmitCompilerGeneratedFiles但生成器抛了异常在 VS 里打开“生成 运行”日志看有没有RS1035等生成器诊断生成了文件但是内容为空ForAttributeWithMetadataName没匹配上特性名确认特性全名是FUI.FUIComponentAttribute而不是FUIComponentAttribute简写代码生成成功但业务访问不到生成的类不是public检查是否加了internal业务组装件需要InternalsVisibleTo尤其是第一点很多初学者漏了OutputItemTypeAnalyzer ReferenceOutputAssemblyfalse。如果这里没写对项目会把生成器当成普通程序集引用而不是分析器导致Initialize方法根本不会被调用。4.3 调试 Source Generator 的两种实用手段生成器代码有个特点它运行在编译器进程内不能像普通业务代码那样打断点。调试起来特别头疼我用过的有效方法有两个。第一个也是最粗暴的是在生成器代码里加Debugger.Launch()public void Initialize(IncrementalGeneratorInitializationContext context) { #if DEBUG if (!System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Launch(); } #endif // ... }编译时如果生成器被调用系统会弹出“选择调试器”窗口然后就能像普通代码一样下断点看中间变量。这个方法在调试GetComponentTarget里语义模型相关操作时非常好用。第二个是记录日志。生成器项目往临时文件写日志File.AppendAllText( Path.Combine(Path.GetTempPath(), fui-generator.log), $[{DateTime.Now:HH:mm:ss}] target{fullyQualifiedType} kind{kind} key{key}\n);虽然生成器运行在内存里但它确实有文件系统权限。日志文件路径写死到临时目录排查完记得删掉不然生产环境也会留日志虽然不是故障但也不干净。4.4 编译期异常代码在大型项目里的表现我把这套方案接入 FUI 项目后跑过一次包含 400 多个带特性类型的编译。增量编译时生成器 Pipeline 命中缓存整体编译速度和纯手写注册时代持平冷启动编译会多花 300 毫秒左右这个成本在大型代码库里完全能接受。对比之前反射注册的运行时开销性能收益相当可观。我记录了一下同一台开发机上、同一个页面的冷启动时间改造前约 1.8 秒改造后约 0.9 秒几乎减半。这个提升主要来自启动时不再扫描程序集和实例化所有组件而是延迟到页面真正打开时才创建对应组件对象。开发期体验提升更明显随手删掉一个组件类的实现接口编译立刻报FUI1001根本轮不到运行时。5. 处理与反射注册兼容的迁移策略5.1 双轨并行灰度迁移老项目改造成本不是零如果强制一刀切切换到生成器方案出问题不好定位。我采用的做法是双轨并行业务代码仍保留旧的反射注册入口但把底层存储改为同一套IFUIRegistry接口生成器的注册结果优先注入反射注册只做兜底。这样做的好处很明显原有功能不回退新功能可以先在少量页面试用确认没问题再把反射注册的入口拆掉。具体做法是在启动代码里加一个编译常量public static class FUIBootstrap { public static void Configure() { #if USE_SOURCE_GENERATOR FUIRegistrations.RegisterAll(Registry.Instance); #else ReflectiveRegistration.ScanAll(Registry.Instance); #endif } }USE_SOURCE_GENERATOR条件编译符号可以只在 Debug 模式打开一部分等稳定后全量切过去。5.2 如何让业务方无感升级FUI 框架的对外 API 是一个FUIBootstrapper。业务方只调用FUIBootstrapper.Initialize()具体内部是反射还是生成器业务方完全不用关心。但这里有个隐性要求不能指望业务方在类上多打一个特性。已有的老代码没有FUIComponentAttribute怎么处理我在生成器里加了一个兼容策略当某个类实现了IFUIComponent但没有打任何特性时生成器也把它纳入注册名单Key 默认为类名。这样老代码天然被兼容不需要业务方做任何改动。缺点是这会稍稍增加生成器扫描范围。不过用ForAttributeWithMetadataName写的老代码没特性匹配不到所以这个兼容逻辑要额外把扫描范围扩大到所有实现IFUIComponent的类。代码大概长这样var allComponents context.SyntaxProvider .CreateSyntaxProvider( static (node, _) node is ClassDeclarationSyntax, static (ctx, _) { var symbol ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol; if (symbol?.AllInterfaces.Any(static i i.ToDisplayString() FUI.IFUIComponent) true) { return new ComponentTarget { ... }; } return null; }) .Where(static target target is not null);但这里要注意CreateSyntaxProvider的性能比ForAttributeWithMetadataName低不少。如果项目里类特别多建议加编译常量开关迁移完成后就把这段兼容代码关掉。5.3 迁移期最容易踩的三个坑先说第一个坑老代码里有些组件是用反射注册时通过Activator.CreateInstance(type, args)传构造参数的。切到生成器后这些构造参数就没有地方给了。我的处理方式是先把这些类改成无参构造把参数移到属性上通过PropertyInject或Initialize方法注入。第二个坑是全局程序集缓存。Assembly.GetTypes()在旧代码里还承担了一个隐藏功能某些模块通过反射扫描来“自动发现”外部扩展程序集。切到生成器后这些外部扩展程序集不再被加载功能直接静默失效。必须把外部扩展的注册方式改成显式调用FUIRegistrations.RegisterAssembly(...)。第三个坑是源码生成器的可空上下文。如果特性里Key是string?生成出来的代码可能包含可空警告。需要在生成的代码文件头加#nullable enable或#nullable disable不然dotnet build -warnaserror一开生成代码直接变成错误。我统一在文件头写了#nullable enable并保证生成的代码里所有Key都是合法非空字符串为空时给默认值。6. 实测数据与优化收益6.1 启动耗时对比我在三台不同配置的机器上跑过对比测试取其中一台中端笔记本的数据场景反射注册Source Generator优化幅度冷启动 500 组件1860 ms920 ms50.5%热启动 500 组件720 ms540 ms25%首次编译100%基线280 ms多 280 ms增量编译100%基线持平基本无感冷启动省下的时间主要来自去掉程序集全量扫描和实例化开销。因为这 500 个组件并不都是 App 启动就要用很多是二级页面、设置页面、主题面板反射注册会在启动时把它们全部new一遍纯浪费。6.2 代码体积与可读性生成器方案会让程序集体积略微增加因为多了一段注册代码。但这段代码是普通的call指令不是反射调用的字符串表所以 IL 反而会更紧凑。在我测试的项目里程序集体积从 4.2 MB 降到 4.0 MB少了 200 KB 左右主要是反射相关的元数据和字符串池。6.3 错误发现时间对照这个维度很难量化但从体验上差异巨大。我整理了一张开发过程事故对照表错误类型反射注册Source Generator组件类缺少无参构造运行时报MissingMethodException设计期/编译期直接报FUI1001或 CS 错误路由 Key 拼错编译期无感知运行时 404编译期不报错Key 是字符串但统一字典可查同一路由重复注册静默覆盖难察觉编译期报FUI1002组件类删除后未清理注册编译期无感知运行时缺失编译期报FUI1003待生成代码时可检测组件实现接口缺失运行到初始化才崩编译期报FUI1001这能直观地说明“编译期异常”到底带来了什么开发阶段就把 95% 的装配类问题拦截掉剩下的 5% 基本都是 IDE 不刷新导致的假性错误。7. 扩展思路让 Source Generator 走得更远当前 FUI 的生成器只做了最核心的注册代码生成。但既然管线已经跑通了完全可以往更多方向扩展。我整理了几个值得做的方向其中前两个我已经实际接入。7.1 自动生成路由表元数据FUI 里有个RouteTable负责把 URL 路由映射到组件。原来也是反射加载现在生成器可以直接把路由和类型的对应关系生成到一个ReadOnlyDictionary。这样不仅省了反射还让RouteTable在编译期就具备完整性校验只要存在某个Key login的组件就一定会出现在路由表里不会出现在运行时打开页面才 404 的情况。7.2 生成依赖注入的绑定代码如果 FUI 框架内部用到了依赖注入容器那IDependencyRegistry的绑定逻辑也可以生成。原本typeof(IService).GetInterfaces()扫描部分直接删掉生成器输出builder.AddScopedILoginService, LoginService(); builder.AddSingletonIThemeManager, ThemeManager();同样的原理错误从运行时提前到编译期而且对 Hot Reload 友好因为绑定逻辑不会再在启动时被反复重建。7.3 与 AOT 发布结合最近 .NET 的 AOT 发布越来越主流而反射在 AOT 下会受限。Source Generator 天然适合替代反射所以把 FUI 生成器方案做成 AOT 兼容是特别有价值的方向。生成器里不出dynamic、不出运行时字符串拼接所有类型信息都在编译时确定理论上 AOT 能直接剪裁掉反射相关的所有分支。关于运行环境有一点要提醒生成器本身跑在编译器进程里所以不能引用任何依赖业务程序集的库只能引用Microsoft.CodeAnalysis和 .NET 标准库。生成器项目里的依赖要尽量干净否则会把一堆运行时依赖带进编译环境导致复杂版本冲突。我见过同事把Newtonsoft.Json直接引到生成器项目里结果和编译器进程里另一个版本的Newtonsoft.Json打架查错查了一下午。7.4 设计一个小的扩展框架生成器要支持的注册类型越多代码就越乱。我在 FUI 生成器里做了一个小的扩展机制每种注册类型对应一个IRegistrationWriter生成器遍历注册目标时按类型分发到对应 writer。internal interface IRegistrationWriter { void Write(CodeWriter writer, ComponentTarget target); } internal sealed class ComponentRegistrationWriter : IRegistrationWriter { public void Write(CodeWriter writer, ComponentTarget target) { writer.WriteLine($registry.RegisterComponent{target.FullyQualifiedType}(\{target.Key}\);); } }这样以后新增一种注册类型只需要写一个新的IRegistrationWriter实现然后注册到 writer 工厂里生成器主体代码完全不用动。这种小设计对个人项目帮助巨大维护成本肉眼可见地下降。8. 迁移之后的一些个人心得在 FUI 上完成这次从反射到 Source Generator 的装配改造后有几个比较深的体会。第一点是真正改善开发体验的不是“更快”而是“更早”。启动时间从 1.8 秒降到 0.9 秒当然舒服但让我觉得这套方案值回票价的是编译期把那些装配错误直接变成红色波浪线。以前业务方提 bug 单大多数都是“某个页面打不开”“某个皮肤渲染失败”追根溯源全是装配阶段的问题。现在这些问题在 CI 阶段就被挡住几乎不会流转到业务方和用户手上。第二点是Source Generator 的学习曲线比想象中陡但一次投资终身受益。只要理解了管线里数据是怎么从语法树、语义模型流到SourceText的后面再写什么生成器都很快。Roslyn 这套 API 设计得还算清晰踩坑的主要来源其实是 MSBuild 集成和缓存语义不是生成器本身。第三点是代码生成器的输出一定要可读。你生成出来的.g.cs是给人看的不是给机器看的。保持缩进规范、命名清晰、注释到位未来排查问题时你会感谢当时的自己。我就是因为在CodeWriter上多花了一个小时后面排查生成代码问题时基本没走弯路。如果你也在维护带有反射注册逻辑的框架或组件库我非常建议花一两个周末把这块改成编译期装配。改动范围不大收益却非常直接。从反射注册到 Source Generator 这一跳真正把 FUI 的装配问题从“运行时崩溃”变成了“编译期异常”而后者包含的信息量和工作效率完全不是一个量级。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。