news 2026/10/1 12:49:54

在.NET上用源生成器复刻Lombok:自动注入与构造器实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在.NET上用源生成器复刻Lombok:自动注入与构造器实战

1. 为什么我决定在.NET上"复刻"一个Lombok

1.1 从Java迁移到.NET的第一感受:样板代码把人都淹没了

去年我带一个项目从Java技术栈往.NET迁移,团队里几个写惯了Spring Boot + Lombok的老哥第一天就开始抱怨:"C#怎么连个@Slf4j都没有?"我当时还不以为意,觉得C#有属性有字符串内插,样板代码能多到哪去?结果真动起手来才发现,问题比想象中大得多。

拿一个最常见的业务服务类来说,你要写依赖注入的构造函数、要写日志字段、要写查询参数的Builder、要写DTO的映射、要写一堆Equals和GetHashCode……这些代码有一个共同点:语义上完全确定,但你必须手工逐行敲出来。敲完你自己都分不清到底哪些是有业务逻辑的代码,哪些只是给编译器看的"搬运工代码"。代码评审的时候,满屏都是构造函数赋值和日志初始化,真正需要人看的那几行反而被淹没在噪音里。

Java圈子的Lombok对这个问题的解法是用注解在编译期生成字节码,@RequiredArgsConstructor直接生成带参构造,@Slf4j直接生成log字段,@Builder直接生成链式构造器。C#其实不缺类似的机制——**源生成器(Source Generator)**就是干这个的最好工具,只不过国内讨论得少,很多人还停留在"听说过Roslyn但没用过"的阶段。所以这个项目的名字就叫"打造.NET平台的Lombok",目标很朴素:把模板代码从人的肩膀上挪到编译器的自动流水线上。

1.2 Lombok到底做对了什么,以及它为什么没法直接搬过来

Lombok做对的核心事情不是"减少敲键盘",而是让代码意图和代码实现分离。你写一个@Builder,读代码的人一眼就知道这个类支持构造者模式,而不需要从头到尾看一遍几十行的WithXxx方法才能确认这个类的形态。这种"声明式编程"的好处是长期维护的,你的代码量少了,阅读成本也低了,出错的概率自然下降。

那为什么不直接用Java那套思路在.NET上照搬?原因有三。

第一,C#和Java的编译管线不同。Lombok靠的是Java编译器内部的注解处理器钩子,直接在AST上改字节码;而.NET这边对应的"钩子"是Roslyn源生成器,它只能向编译工程里追加文件,不能修改已有的类定义。你没法在编译中间阶段给某个类"注入"一个构造函数进去,只能生成一个partial类文件,通过partial关键字把生成的方法和成员"拼"到同一个类型里。

第二,两个生态的语法习惯不一样。Java里@Value、@Data那一套偏重POJO的getter/setter生成,C#有属性语法,这块需求天然少了一大半;C#更缺的是依赖注入构造、日志、Builder、映射这些偏"服务端工程化"的样板代码。所以做.NET版Lombok,功能优先级和Java是完全不同的。

第三,微软官方其实给了一部分答案,比如record类型自带Equals/GetHashCode/ToString,主构造函数在C# 12里也能省掉一部分字段赋值。但record解决不了全部问题,尤其是构造函数注入和日志这类涉及依赖注入容器、生命周期管理的场景,社区一直没有标准做法。这反而说明这块空白值得自己动手填。

1.3 方案选型:源生成器、T4模板、IL织入、动态代理怎么选

在真正动手前,我列了一张表,把可行的几条技术路线过了一遍:

技术方案实现原理优点主要问题
Roslyn 源生成器编译期分析语法树/语义模型,生成partial代码文件IDE智能提示、AOT友好、无运行时依赖学习曲线稍陡,调试麻烦
T4 模板在VS里生成代码文本文件老牌、资料多只在IDE保存时生成,CI里配置繁琐,无法感知语义
IL织入(Mono.Cecil等)编译后改写程序集能改已有类型调试困难、与AOT不兼容、步骤繁琐
动态代理 / 反射运行时生成代理类灵活性能损耗、调试困难、与原生类型有隔阂

最终的结论很明确:源生成器是唯一一个既能参与编译期语义分析、又不引入运行时依赖、还能让用户看到生成代码的选项。它生成的代码就是普通的C#,出错时能断点进去单步看,这对团队落地太重要了——我可以理直气壮跟同事说"这工具生成的东西你随时能在目标目录里查到,不是黑盒魔法"。

选型确定后,剩下的问题就是工程细节了。下面的内容我按实现顺序讲,从工程骨架到三个核心功能(构造函数注入、日志注入、构造者模式),再到调试和打包,全程是我实际跑通的结果,不是从文档里抄的。

2. 源生成器工程骨架:项目结构、SDK配置和调试链路

2.1 一个生成器类库加一个消费项目的标准结构

源生成器的工程结构和普通类库不一样,至少需要两层:

  • Generator项目:生成器本体,必须目标框架netstandard2.0,因为这样才能被VS和MSBuild的Roslyn编译器加载,不管下游是用.NET 6还是.NET 8。
  • Consumer项目(示例/测试项目):真正引用生成器、使用[AutoInject]这些标签的库或可执行程序。

我是这么组织的:

DotNetLombok.sln ├── src/DotNetLombok.Generator // 源生成器本体 │ ├── AutoInjectGenerator.cs │ ├── AutoLoggerGenerator.cs │ └── AutoBuilderGenerator.cs ├── samples/BasicUsage // 示例消费项目 │ ├── Services/OrderService.cs // 标记了特性的类 │ └── Program.cs └── tests/Generator.SnapshotTests // 快照测试(可选但强烈推荐)

Generator项目的csproj有几个关键点,网上抄来的配置往往漏掉Analyzer这行:

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

消费方引用的写法才是核心,很多人栽在这里:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\..\src\DotNetLombok.Generator\DotNetLombok.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup> </Project>

这里OutputItemType="Analyzer"表示把该项目当作分析器传给编译器,ReferenceOutputAssembly="false"表示不把这个程序集加进消费方的运行时依赖。少任何一个,生成器要么不跑,要么会把生成器本身dll带进生产产物,等于把一个编译期工具打进了运行时,属于低级错误。

2.2 IIncrementalGenerator的核心写法:从语法树到代码输出

现在生成器推荐用IIncrementalGenerator,老式的ISourceGenerator已经过时且缓存性能差。核心思路可以理解成一条流水线:先找标记 → 再做语义校验 → 最后产出代码。

拿构造函数注入生成器举例,我第一个版本的管线是:

[Generator(LanguageNames.CSharp)] public sealed class AutoInjectGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var provider = context.SyntaxProvider.ForAttributeWithMetadataName( "DotNetLombok.AutoInjectAttribute", static (node, _) => node is ClassDeclarationSyntax or RecordDeclarationSyntax, static (ctx, _) => (ctx.TargetSymbol as INamedTypeSymbol)!); context.RegisterSourceOutput(provider, static (spc, typeSymbol) => { if (typeSymbol is null) return; var code = InjectCodeGenerator.Generate(typeSymbol); spc.AddSource($"AutoInject_{typeSymbol.ToDisplayString().Replace('<', '[')}.g.cs", SourceText.From(code, Encoding.UTF8)); }); } }

ForAttributeWithMetadataName是.NET 6之后提供的高性能API,它直接把"标记了某特性的类型的语义符号"筛出来,省掉了我以前手写RegisterSyntaxNodeProvider再解析Attribute的笨办法。用它的最直接收益是增量缓存——编译器只在受影响的文件变化时重跑,而不是全量重新生成,这在大型项目里的编译速度差距是秒级和分钟级。

2.3 调试生成器的三板斧:断点、脱机文件和诊断输出

生成器调试起来和普通代码不一样,普通代码F5就跑,生成器要等到编译触发。压箱底的三招:

第一招,让生成器弹调试器。在生成器代码开头加:

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

每次编译都会弹出一个"选择调试器"的对话框,选VS,断点就命中了。这招简单粗暴,但很管用。

第二招,直接看生成的文件。默认情况下,生成的.g.cs文件在obj/Debug/net8.0/generated/DotNetLombok.Generator/DotNetLombok.Generator.AutoInjectGenerator/目录里。我习惯在项目文件里加一段,让VS自动把它们展示出来:

<PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)generated</CompilerGeneratedFilesOutputPath> </PropertyGroup>

打开obj/generated目录,生成代码长什么样一目了然,比什么日志都好使。

第三招,用报告器直接定位错误。如果生成逻辑里有无法处理的边界情况,不要默默return,应该向编译器抛诊断:

spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor("DOTNETLOMBOK001", "无法生成构造函数", "类型 {0} 带有不能注入的字段 {1}", "DotNetLombok", DiagnosticSeverity.Error, isEnabledByDefault: true), location));

这样出错时错误列表里跟着一条带文件名和行号的提示,同事在IDE里就能看到,而不是等运行时报MissingMethodException才开始排查。

3. 构造函数注入:让编译器替你写构造参数和赋值

3.1 目标代码形态:一个特性少写二十行

我要做的效果是,用户定义一个服务类时,不再手写构造和字段赋值,只需要:

[AutoInject] public partial class OrderService { [Inject] private readonly IOrderRepository _repository; [Inject] private readonly ILogger<OrderService> _logger; }

生成器自动产出下面的代码:

public partial class OrderService { public OrderService(IOrderRepository repository, ILogger<OrderService> logger) { _repository = repository; _logger = logger; } }

一眼看上去平平无奇,但想想你维护的几十个服务类,每一个都省掉构造+赋值,累积起来非常可观。我把目标定为"生成构造函数和字段赋值"而不是"用属性注入",是因为构造函数注入是.NET依赖注入的标准推荐做法,配合微软的IServiceCollection,类型的依赖关系在创建时就被固定下来,不可中途改变,既好测也好排查。

3.2 语义模型解析:拿类型、筛字段、拼签名

生成逻辑拆成三步。

第一步,拿到类型符号后,先过滤掉非partial类型。没有partial,生成代码就没法和原类型合并,这一步必须前置校验。

第二步,遍历类型的成员,筛选出带[Inject]特性的字段。这里有个我之前踩过的坑:不能只做词法层面的匹配,比如用ctx.Node.DescendantNodes()找Attribute语法节点,因为可能撞上同名但来自别处的Attribute。正确做法是利用ForAttributeWithMetadataName已经给好的符号,然后用语义模型判断字段类型和可空性。

第三步才是生成代码。生成时需要做几件小事:

  • 字段排序。我按字段在源码中的顺序生成参数,用户声明顺序即依赖注入顺序,这样和手写习惯一致。
  • 参数可空性。string _name和string? _nickName生成出来参数可空修饰必须一致,否则会有nullable警告。
  • 避免参数名冲突。如果字段叫logger,参数也叫logger,赋值为this.logger = logger会非常别扭,我统一给参数名加injected前缀,比如injectedRepository,既避免冲突,又能在代码里隐式表达"这是注入进来的依赖"。

3.3 partial类和继承关系的边界处理

很快会遇到三个绕不开的问题,我逐个说。

第一个是partial类没写全。用户如果在另一个文件里已经手动声明了一个构造函数,生成器再生成一个就必须合并。我采用的做法是生成前检查符号上有没有已声明的构造函数,有就不生成构造函数本体,只生成字段赋值逻辑,通过partial void钩子让用户去接。不过实际用下来感觉这种"灵活"增加了理解成本,最后我反而选择了更严格的规则:类上手动定义构造函数的行为直接报诊断DOTNETLOMBOK002,让用户删掉手写构造或放弃使用[AutoInject]。规则简单了,产品才好用。

第二个是继承场景。如果服务类继承了某个基类,基类有自己的构造参数,子类生成的构造函数必须用base(...)把参数传上去。这一块很难自动判定基类哪些构造参数应该传什么,我的处理很克制:默认不支持带基类参数的自动注入,遇到类有基类且基类不是object时抛诊断,把决定权留给用户。与其生成一个语义可能错误的代码,不如明确说不支持。

第三个是泛型类。Repository<T>这类类型要原样保留泛型参数,生成代码时要把T的约束也接上。代码生成时我用symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)拿全限定名,这比手拼global::前缀安全得多,也不会漏掉Nullable上下文。

3.4 生成效果对照:直接贴手写和生成的差异

我拿一个真实的服务类做对比,感兴趣可以自己抄下来体会。手写版:

public partial class InventoryService { private readonly IInventoryRepository _repository; private readonly IMapper _mapper; private readonly ILogger<InventoryService> _logger; public InventoryService( IInventoryRepository repository, IMapper mapper, ILogger<InventoryService> logger) { _repository = repository; _mapper = mapper; _logger = logger; } public async Task<InventoryDto> GetAsync(int id) { var entity = await _repository.GetByIdAsync(id); return _mapper.Map<InventoryDto>(entity); } }

生成器版(用户手写的部分):

[AutoInject] public partial class InventoryService { [Inject] private readonly IInventoryRepository _repository; [Inject] private readonly IMapper _mapper; [Inject] private readonly ILogger<InventoryService> _logger; public async Task<InventoryDto> GetAsync(int id) { var entity = await _repository.GetByIdAsync(id); return _mapper.Map<InventoryDto>(entity); } }

字段声明仍然显式写,这是故意的。隐式字段会导致代码可读性断崖式下降,同事看你这个类根本不知道依赖有哪些。透明、可预期,是这类代码生成工具在设计上必须守住的底线。

4. 日志注入与构造者模式:把两个高频样板按模板化

4.1 日志注入:不硬编码日志框架的懒加载方案

日志注入看起来简单:生成一个ILogger字段就行了。但实际设计时有一个隐藏问题:ILogger<T>里的T必须是使用该日志的类。如果生成器给OrderService生成了ILogger<OrderService>,那就要默认用户一定注入了日志框架。这在某些纯领域项目里根本不存在,硬塞进去就是不可控的依赖。

我的设计是这样:生成器只生成一个IsExternalInit风格的日志属性占位,采用延迟获取模式,不向构造函数里添加参数,也不强制要求DI容器提供ILogger:

public partial class OrderService { private ILogger<OrderService>? _logger; protected ILogger<OrderService> Logger => _logger ??= Microsoft.Extensions.Logging.LoggerFactory .Create(builder => builder.AddConsole()) .CreateLogger<OrderService>(); }

等等,我实际测试后发现直接LoggerFactory是坑——如果服务端已经配置了结构化日志、日志级别过滤,你这么创建会绕过全局配置,日志行为会跟项目里其他地方不一致。所以我最后改成了可替换的注入点:默认生成如下结构:

public partial class OrderService { private ILogger<OrderService>? _logger; protected ILogger<OrderService> Logger => _logger ??= CreateLogger(); partial void CreateLoggerPartial(ref ILogger<OrderService> logger); }

用户觉得有需要时,自己用partial void CreateLoggerPartial(...)去接DI容器;不接则用一个安全的默认实现。这么一来,特性不绑架框架,项目里没配日志也能编译,配了日志也可以通过partial方法无缝接入。

4.2 构造者模式:C#的链式API怎么生成才顺手

构造者模式在Java世界靠@Builder,在C#这边其实一直没有标准方案。手写一个Builder动辄几十行,而且改一个字段要改三处:原类的私有构造/属性初始化、Builder的字段声明、Builder的赋值方法。源生成器做这件事的收益比构造注入和日志还明显。

我期望的用法是:

[AutoBuilder] public partial class SearchRequest { public string? Keyword { get; set; } public int PageIndex { get; set; } = 1; public int PageSize { get; set; } = 20; }

生成器产出一个SearchRequestBuilder:

public sealed class SearchRequestBuilder { private string? _keyword; private int _pageIndex = 1; private int _pageSize = 20; public SearchRequestBuilder WithKeyword(string? keyword) { _keyword = keyword; return this; } public SearchRequestBuilder WithPageIndex(int pageIndex) { _pageIndex = pageIndex; return this; } public SearchRequestBuilder WithPageSize(int pageSize) { _pageSize = pageSize; return this; } public SearchRequest Build() => new SearchRequest { Keyword = _keyword, PageIndex = _pageIndex, PageSize = _pageSize }; public static SearchRequestBuilder Create() => new SearchRequestBuilder(); }

设计上我有三条原则:

  • 可变集合属性不直接用字段默认值。如果属性是List<string> Tags,生成字段直接new List<string>(),否则用户调Build()拿到的集合永远是一个共享实例,改一个对象会污染其他对象。
  • 值类型字段的默认值跟着属性初始化器走。PageIndex = 1意味着Builder里对应字段初始值也得是1,我用属性初始化器的常量做初值,避免"看起来默认值一致、实际生成对象取值是0"的隐性差异。
  • 链式方法命名只提供WithXxx。不要再去生成SetXxx。C#团队已经习惯record的with语法,WithXxx最直观,不需要两套方法增加体积。

4.3 生成粒度上的取舍:宁可少生成,不要瞎生成

这三个特性的实现过程中,我反复拿捏一个原则:生成器不是万能的,不该为所有业务场景变魔法。遇到下面情况我会选择不生成或报诊断而不是硬生成:

  • 类里有只读字段但没有定义初始化器,同时属性面向UI需要无参构造——这种语义冲突我不擅自决定。
  • 类层级过深(三层以上继承)的Builder——复杂继承下的Builder本来就不该无脑生成,人工梳理依赖关系更靠谱。
  • 泛型类型参数上加了标记——虽然技术上能把T原样拷贝,但涉及约束复合(比如where T : class, new())时很容易生成出不能编译的代码。我目前对泛型类生成Builder保持谨慎,先做非泛型主场景。

克制放在第一位,生成器是替你省时间的,不是替你挖新坑的。一个生成的代码如果用户要花一倍时间去调试,那不如不生成。

5. 调试源生成器时绕不开的坑:缓存、断点和"改了没生效"

5.1 改了源码,生成结果却纹丝不动的三大来源

开发和调试源生成器,最劝退的就是"我明明改了标记A,生成结果没变化"。我统计过自己三次犯同一个错误的来源:

现象原因解决办法
改了特性名,VS里没有反应bin/obj缓存没清重启VS,或手动删obj和.vs目录
断点根本没进生成器生成器dll被MSBuild进程加载过一次,后面一直在用旧dll结束所有dotnet.exe/MSBuild.exe进程或重启VS
生成代码和当前编译环境对不上IntelliSense用的是上一次编译的缓存dotnet build /t:Rebuild强制全量

其中最坑的是第二个。VS的Roslyn组件会常驻MSBuild进程,你改了生成器代码重新编译后,dll更新了,但进程里加载的还是旧版本。你会发现"加了Debugger.Launch怎么不弹窗了",多半就是旧进程在干活。稳妥的办法是把VS彻底重启一次,别只build,build往往不够。

5.2 用诊断输出当"日志",让生成过程可见

生成器代码里Console.WriteLine是看不到的,因为编译器不在控制台环境跑你的代码。我后来养成一个好习惯:一切运行信息都走Diagnostic通道。

我在生成器里定义一个DiagnosticDescriptor,Severity设为Info,IsEnabledByDefault设为true,在关键节点输出:

spc.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor("DOTNETLOMBOK_DEBUG", "生成器调试", "正在为类型 {0} 生成构造函数,找到注入字段 {1} 个", "DotNetLombok", DiagnosticSeverity.Info, true), null, typeSymbol.Name, fieldCount));

这样在VS的错误列表里,把"生成器/ Analyzer"筛选打开,就能看到一条条Info信息。比断点还方便,因为它是全量编译视角,能确认"到底哪个类进入了生成器"。

5.3 增量缓存:别让"高效API"反过来坑你

IIncrementalGenerator的API设计是为了编译缓存,但它会把你用来生成代码的"输入参数"当成缓存key。如果输入的是一个不可比较的对象,Roslyn可能直接放弃缓存;如果输入的是带DateTime.Now这类不稳定值的自定义对象,则会产出错误缓存。

我的经验是对生成器的输入模型保持纯粹:**把语义符号转换成自定义的、只包含值类型的EquatableModel**再进入生成环节。比如:

record struct InjectableField(string Name, string Type, bool IsNullable, Accessibility Access);

缓存比较时只比对字符串和枚举,稳定、可预期。这也让快照测试变得好写——生成的代码只依赖模型值,不依赖Roslyn内部的引用,测试时构造模型就能断言输出。

我在这个阶段还发现一个容易犯的错:生成代码里写了绝对路径或时间戳。比如有人图方便在代码头部加// generated at 2025-...,这种信息会污染仓库版本,每次编译都产生diff,对增量构建是灾难。生成代码必须幂等、确定性,否则所有快照测试都会变红。

6. 打包发布与团队落地:从"自己爽"到"同事愿意用"

6.1 打包成NuGet源生成器的正确姿势

工具在自己项目里跑通只是第一步,要让团队真正用起来,最好打成NuGet包。源生成器的打包和普通类库有区别,普通类库默认把编译产物放进lib/,生成器要放进analyzers/dotnet/cs目录:

<PropertyGroup> <IncludeBuildOutput>false</IncludeBuildOutput> <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking> </PropertyGroup> <ItemGroup> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup>

SuppressDependenciesWhenPacking很关键,它避免把生成器依赖的Microsoft.CodeAnalysis.CSharp也打进依赖项列表,因为生成器运行时编译器会自己提供Roslyn程序集,不需要也不应该单独引用。

6.2 多目标框架与IDE版本的兼容问题

生成器本身目标netstandard2.0是为了被老版本Roslyn加载,而Roslyn版本取决于IDE。我测试过用Microsoft.CodeAnalysis.CSharp 4.8.0编译的生成器,在VS2022预览版和.NET 8 SDK下都正常,但netstandard2.0加上LangVersion=latest后,代码里一旦用到较新的C#语法(比如record struct、list patterns),生成器自身的dll就会要求较新的Roslyn运行时去解析——生成器自身代码的语言版本不能太激进。

我的惯例是生成器内部代码尽量停留在C# 10左右的保守语法,生成的目标代码则可以根据消费方框架自动判断版本特性。消费方是.NET 6就用file作用域命名空间,消费方是.NET Framework就用显式命名空间,这个判断在生成代码时用context.ParseOptions去拿语言版本即可。

6.3 团队落地的三条经验:命名、可见性、审查边界

这个工具实际在团队里跑了两个月,留下三条经验,比代码本身更值钱。

第一,特性名和命名空间要短、要显眼。统一namespace DotNetLombok,特性名不带后缀:[AutoInject]、[AutoLogger]、[AutoBuilder]。同事一眼就知道这是生成器在起作用,而不是某个业务框架的行为。

第二,生成代码必须能被审查。我要求所有生成代码都在obj/generated目录里可查,并且写进CI的核心验证环节:跑一份快照测试,任何一个生成结果和预期不一致,构建直接失败。这样生成器改了不会无声无息影响线上,同事也放心。

第三,给"不用生成器"留出路径。有的同事就是喜欢显式写构造函数,你按头让他用工具反而会引发对立。我在IntelliSense代码片段库里准备了一份手写模板,谁想手写就手写,生成器只是选项,不是强制规约。工具推广最大的阻力从来不是技术,而是信任。

7. 回头看:这个项目到底给我和团队省了什么

写到这里,整个工具的核心实现已经讲完。回过头看,这个项目最大的价值不在"少敲了几千行代码",而在它逼着我把"哪些代码是模板、哪些代码是逻辑"分了个清清楚楚。以前写服务类,脑子要装着构造、日志、Builder一堆杂事,现在只关注业务方法本身,读代码的人同样只需要看业务方法。

如果打算在自己的项目里复刻,我会建议从[AutoInject]开始,它是三个功能里收益最明显、边界最清晰的;跑通之后再扩展[AutoBuilder],最后再碰[AutoLogger]这种涉及框架上下文的功能。每加一个特性之前,先在纸上把"不支持什么"写清楚,比急着把功能做出来重要得多。

我最后一个小小的体会:源生成器这东西,第一次跑通的时候你会很兴奋,第二次改出问题的时候你会骂娘,但真正把它放进日复一日的工作流里,就会发现它和那些基础设置一样——平时没什么存在感,可一旦拿掉,立刻觉得浑身别扭。这就是好的开发工具该有的样子。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:49:53

PySide6桌面应用开发全指南,从环境搭建到打包发布

PySide step by step系列&#xff0c;说到底就是要把“用Python做桌面软件”这件事从头到尾走完整&#xff1a;装环境、写第一个窗口、理解布局和控件、掌握信号槽、最后把程序打包成一个能发给别人的exe。网上讲PySide的教程不少&#xff0c;但大多数要么是官方demo的翻译&…

作者头像 李华
网站建设 2026/10/1 12:49:52

单片机课程设计实战:16x16点阵汉字电子显示屏设计与实现

最近帮一个学弟看课程设计的开题内容&#xff0c;题目恰是“基于单片机的点阵式汉字电子显示屏的设计”&#xff0c;这让我一下想起当年自己焊板子、调时序、被残影折磨的日子。说实话&#xff0c;这个题目在单片机课程设计和电子设计入门项目里非常典型&#xff1a;它把IO控制…

作者头像 李华
网站建设 2026/10/1 12:49:28

Wigner-Hough变换实战:低信噪比LFM信号检测与参数估计

简介&#xff1a;这份资源围绕Wigner-Hough变换展开&#xff0c;面向从事非平稳信号处理、时频分析与故障诊断的工程师及科研人员&#xff0c;帮助理解如何将Wigner分布与Hough变换结合&#xff0c;以抑制交叉项干扰并检测时频域中的显著特征。压缩包共4个文件&#xff0c;以3个…

作者头像 李华
网站建设 2026/10/1 12:49:24

Spring Boot快餐订餐系统实战:从需求拆解到JWT鉴权与Redis缓存落地

写了这么多年Java后端&#xff0c;每年五六月份都会被问一次“基于Spring Boot的快餐订餐系统怎么做”。这个题目在计算机毕业设计里属于典型的“常青树”&#xff1a;业务上不难理解——点餐、下单、支付、出餐&#xff0c;是一条非常标准的交易链路&#xff1b;技术上又不简单…

作者头像 李华
网站建设 2026/10/1 12:48:59

CMake 入门与嵌入式迁移:从环境配置到构建烧录全解析

1. 从那一行红色报错说起&#xff1a;cmake 到底是什么东西第一次在 Windows 的 PowerShell 里敲下cmake这四个字母&#xff0c;回给你的大概率是这么一行红字&#xff1a;cmake : 无法将“cmake”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这句话看着像在骂人&…

作者头像 李华
网站建设 2026/10/1 12:48:57

集成测试计划制定指南:从策略选择到基线冻结的完整步骤

做集成测试计划这件事&#xff0c;看起来就是把测试范围、时间和人员排一下&#xff0c;但真正上手之后你会发现&#xff0c;排不好的计划要么在等别人的模块&#xff0c;要么被环境问题反复打断。我参与过几套内部系统的集成测试&#xff0c;也独立负责过跨团队联调的计划编制…

作者头像 李华