模块六 · AI 协作与表达
AI 协作与代码评审:C# 项目里的用法与审法
.NET 的隐式约定多——DI 生命周期、可空引用、async 状态机、EF Core 延迟执行。AI 写对了语法,却常在这些地方悄悄错。
读完这一节,你能做到
- 说清 .NET 项目里 AI 最容易踩的四个隐式约定。
- 用带框架版本、DI 生命周期、EF 配置的上下文一次要出能用的代码。
- 按「契约 → 失败路径测试 → 实现 → 生命周期审查」推进任务。
- 用五个检查点审出 captive dependency、线程池饥饿、N+1。
- 用编译、单测与集成测试证明改动没有变坏。
一、.NET 的隐式约定最多
C# 语法清晰,但框架层有一堆"约定俗成":DI 的生命周期、可空引用的契约、async 的状态机、IQueryable 的延迟执行。AI 生成的代码在语法上往往无可挑剔,问题都藏在这些约定里。
| 它很擅长 | 它经常错 |
|---|---|
| Controller、DTO、校验特性 | DI 生命周期混用(Scoped 注入 Singleton) |
| LINQ 查询写法 | EF Core 跟踪与 N+1 |
| 常见异步模式 | async void、.Result、CancellationToken 只挂签名 |
| 配置与脚手架 | 中间件顺序与异常处理位置 |
判断原则:语法对不等于约定对。 生命周期和延迟执行错了不会报错,只在高并发或大数据量下暴露。
二、上下文:把框架版本与生命周期写进去
- 目标:哪个接口/服务、验收看什么。
- 现状:.NET 与 ASP.NET Core 版本、是否开可空引用类型、EF Core 版本与是否有全局查询过滤器、DI 注册在哪里、日志与异常处理中间件的位置。
- 约束:不新增包、必须走现有
Result<T>、只读查询AsNoTracking、禁止async void。 - 验收:行为级——重复请求幂等、取消能中断、单次查询 SQL 条数上限。
反例:帮我写一个订单列表接口。
正例:目标:订单列表接口,支持分页与关键词筛选。
现状:.NET 8 + EF Core 8,可空引用已开,异常处理中间件在管道最前,结果统一 Result<T>。
约束:不新增包;只读查询 AsNoTracking;禁止在循环里查;必须传 CancellationToken。
验收:单次请求 SQL ≤ 3 条;取消时能立即中断;空结果返回空数组而不是 null。
三、拆任务:契约先行,实现与审查分开
① 契约(DTO / 结果类型 / 异常语义)
② 失败路径的测试(未找到、参数非法、取消)
③ 实现(查询、映射、分页)
④ 生命周期与查询审查(单独一轮)
第三步与第四步必须分开:功能改动和"改 DI 注册""加 AsNoTracking"混在一个 diff 里,你就很难判断哪一处引入了新问题。
粒度标准不变:10 分钟能逐行看完。
四、五个检查点
第一点:DI 生命周期
- Scoped 服务被注入到 Singleton 里了吗?这是 captive dependency,第一次请求后就一直复用同一个实例。
HttpContext被塞进单例或静态字段了吗?在请求结束后会失效。- 注册顺序:
AddScoped/AddSingleton/AddTransient是否与用途匹配。
第二点:异步
- 有
async void吗?异常直接打到线程池,没人接得住。 - 用了
.Result或.Wait()吗?同步阻塞异步,高并发下线程池饥饿。 CancellationToken是一路传下去,还是只在签名上挂了一个?客户端断开后任务还在跑。- 有没有
Task.Run包一个本来就是异步的调用?只是多一次线程切换。
第三点:EF Core 与查询
- 循环里查(N+1)?用
Include或投影一次拿回来。 - 只读查询加了
AsNoTracking吗?不加是白付跟踪开销。 - 理解延迟执行吗?
IQueryable在ToList()之前都没发 SQL,拼接条件不会多查询。 - 投影用了
Select还是把整实体拉回来再手动映射?前者能少查列。
第四点:可空与相等性
- 开了可空引用类型后,
string与string?是两种契约;出现!抑制符就是在绕过检查。 ==、Equals、ReferenceEquals的区别,string的==被重载过。record的相等语义基于所有字段,用错地方会带来意外相等。
第五点:可观测与资源
- 日志是结构化输出吗?容器里 grep 纯文本日志基本没法用。
IDisposable的对象都在using里吗?- 关键路径有耗时与请求 id 吗?出问题能不能串起来。
五、验证:三道闸
- 静态闸:
dotnet build+ 分析器 + 可空警告视为错误(<Nullable>enable</Nullable>+TreatWarningsAsErrors)。 - 行为闸:单测覆盖失败路径与取消;集成测试用
WebApplicationFactory跑真实管道。 - 查询闸:打开 EF 日志数 SQL 条数;并发压一次看线程池与连接池指标。
六、算例:一个查询接口的两种做法
任务:订单列表 + 关键词筛选 + 分页,要求 SQL 条数可控。时长示意。
做法 A:一次要完
| 项 | 耗时 |
|---|---|
| 写提示词、等生成 | 2 分钟 |
| review 110 行 | 15 分钟 |
| 发现循环里查客户名(N+1) | 返工 20 分钟 |
| 发现 Scoped 仓储被注入到单例 | 返工 15 分钟 |
| 合计 | 52 分钟 |
做法 B:拆四轮
| 轮次 | 交给 AI 的 | 自己做的 | 耗时 |
|---|---|---|---|
| ① 契约 | DTO 与分页模型 | 定返回结构与空值语义 | 8 分钟 |
| ② 测试 | 用例骨架 | 补取消与未找到分支 | 10 分钟 |
| ③ 实现 | 查询与映射 | 改投影、加 AsNoTracking | 12 分钟 |
| ④ 生命周期审查 | 无(只审) | 过五个检查点 | 8 分钟 |
| 合计 | 38 分钟 |
净收益 = 生成节省的时间 − (审查 + 返工 + 事故成本)
.NET 的返工往往集中在两处:DI 注册和查询投影。它们都不报错,只在上量后暴露。
七、常见坑
- 把
build通过当成验收。 生命周期与查询计划都不在编译器视野里。 - 可空引用开了但用
!到处抑制。 那等于把契约又关掉了。 - 不看 EF 日志。 N+1 在十条数据下完全无感。
CancellationToken只挂签名。 客户端断开后后台仍在跑,资源白烧。- 让 AI 顺手改 DI 注册。 一处注册变化会影响全局,必须单独审。
- 把真实连接串、密钥贴进提示词。 这类内容永远不该出现在公网模型里。
八、面试怎么讲
怎么用:契约先行,先写失败路径与取消的测试,再让它填实现,最后单独审 DI 与查询
怎么验:build(可空警告算错误)+ 单测 + WebApplicationFactory 集成测试 + EF 日志数 SQL
踩过什么坑:它把 Scoped 仓储注册进单例,压测前一切正常,压测后数据串了
换来什么:同一接口从 52 分钟降到 38 分钟,返工集中在 DI 与查询两块
一句加分项:"我审 AI 代码先看 DI 生命周期和 EF 查询,因为这两块不报错,只在上量后出事。"
九、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。
① 自己写
- 写一个订单列表接口:分页 + 关键词筛选,只读查询
AsNoTracking,投影到 DTO,传CancellationToken。 - 写一个后台任务服务,用
BackgroundService+IServiceScopeFactory正确取 Scoped 依赖。
② 让 AI 写同一题
把第 2 题交给 AI,补一句 .NET 8,禁止把 Scoped 服务注入单例,必须处理取消与异常。
③ 对比要盯的点
| 检查面 | 我的版本 | AI 版本 | 差异说明 |
|---|---|---|---|
| DI 生命周期 | |||
| 取消是否传递 | |||
| 查询条数与投影 | |||
| 异常是否被吞 |
④ 说取舍
- 它有没有把 Scoped 服务直接注入到单例?后果是什么?
- 它用
async void了吗?异常会去哪? - 它那版在 1 万条数据时会发多少条 SQL?
十、自测
- .NET 项目里 AI 最容易踩的四个隐式约定是什么?
- captive dependency 是怎么产生的?为什么压测前发现不了?
- 为什么"契约 → 失败路径测试 → 实现 → 生命周期审查"要分开?
- 五个检查点分别是什么?
async void与.Result各自的问题是什么?- "查询闸"具体看哪些指标?
参考答案要点:DI 生命周期、可空引用、async 状态机、EF 延迟执行;Scoped 被单例持有,只在高并发时暴露;功能与审查混在一个 diff 里无法归因;DI、异步、EF 查询、可空与相等性、可观测与资源;异常无人接收、线程池饥饿;SQL 条数、线程池与连接池指标。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★★ AI 生成的 C# 代码,你会重点审什么?
- ★★★★☆ 怎么给 AI 描述 .NET 上下文,才能避免生命周期错误?
- ★★★★☆ AI 写的 async 代码有哪些典型坑?
- ★★★★☆ AI 生成的 EF Core 查询怎么验证性能?
- ★★★☆☆ AI 时代还需要背 GC 与 async 状态机吗?
- ★★★★☆ AI 能写完接口,为什么还要盯 DI 与取消令牌?
提示:读完可以顺手做几道对应题目,检验是否真的理解