知

模块六 · AI 协作与表达

第 10 / 11 节19 分钟阅读型

AI 协作与代码评审:C# 项目里的用法与审法

.NET 的隐式约定多——DI 生命周期、可空引用、async 状态机、EF Core 延迟执行。AI 写对了语法,却常在这些地方悄悄错。

读完这一节,你能做到

  1. 说清 .NET 项目里 AI 最容易踩的四个隐式约定。
  2. 用带框架版本、DI 生命周期、EF 配置的上下文一次要出能用的代码。
  3. 按「契约 → 失败路径测试 → 实现 → 生命周期审查」推进任务。
  4. 用五个检查点审出 captive dependency、线程池饥饿、N+1。
  5. 用编译、单测与集成测试证明改动没有变坏。

一、.NET 的隐式约定最多

C# 语法清晰,但框架层有一堆"约定俗成":DI 的生命周期、可空引用的契约、async 的状态机、IQueryable 的延迟执行。AI 生成的代码在语法上往往无可挑剔,问题都藏在这些约定里。

它很擅长它经常错
Controller、DTO、校验特性DI 生命周期混用(Scoped 注入 Singleton)
LINQ 查询写法EF Core 跟踪与 N+1
常见异步模式async void、.Result、CancellationToken 只挂签名
配置与脚手架中间件顺序与异常处理位置

判断原则:语法对不等于约定对。 生命周期和延迟执行错了不会报错,只在高并发或大数据量下暴露。

二、上下文:把框架版本与生命周期写进去

  1. 目标:哪个接口/服务、验收看什么。
  2. 现状:.NET 与 ASP.NET Core 版本、是否开可空引用类型、EF Core 版本与是否有全局查询过滤器、DI 注册在哪里、日志与异常处理中间件的位置。
  3. 约束:不新增包、必须走现有 Result<T>、只读查询 AsNoTracking、禁止 async void。
  4. 验收:行为级——重复请求幂等、取消能中断、单次查询 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 吗?出问题能不能串起来。

五、验证:三道闸

  1. 静态闸:dotnet build + 分析器 + 可空警告视为错误(<Nullable>enable</Nullable> + TreatWarningsAsErrors)。
  2. 行为闸:单测覆盖失败路径与取消;集成测试用 WebApplicationFactory 跑真实管道。
  3. 查询闸:打开 EF 日志数 SQL 条数;并发压一次看线程池与连接池指标。

六、算例:一个查询接口的两种做法

任务:订单列表 + 关键词筛选 + 分页,要求 SQL 条数可控。时长示意。

做法 A:一次要完

项耗时
写提示词、等生成2 分钟
review 110 行15 分钟
发现循环里查客户名(N+1)返工 20 分钟
发现 Scoped 仓储被注入到单例返工 15 分钟
合计52 分钟

做法 B:拆四轮

轮次交给 AI 的自己做的耗时
① 契约DTO 与分页模型定返回结构与空值语义8 分钟
② 测试用例骨架补取消与未找到分支10 分钟
③ 实现查询与映射改投影、加 AsNoTracking12 分钟
④ 生命周期审查无(只审)过五个检查点8 分钟
合计38 分钟
净收益 = 生成节省的时间 − (审查 + 返工 + 事故成本)

.NET 的返工往往集中在两处:DI 注册和查询投影。它们都不报错,只在上量后暴露。

七、常见坑

  1. 把 build 通过当成验收。 生命周期与查询计划都不在编译器视野里。
  2. 可空引用开了但用 ! 到处抑制。 那等于把契约又关掉了。
  3. 不看 EF 日志。 N+1 在十条数据下完全无感。
  4. CancellationToken 只挂签名。 客户端断开后后台仍在跑,资源白烧。
  5. 让 AI 顺手改 DI 注册。 一处注册变化会影响全局,必须单独审。
  6. 把真实连接串、密钥贴进提示词。 这类内容永远不该出现在公网模型里。

八、面试怎么讲

怎么用:契约先行,先写失败路径与取消的测试,再让它填实现,最后单独审 DI 与查询
怎么验:build(可空警告算错误)+ 单测 + WebApplicationFactory 集成测试 + EF 日志数 SQL
踩过什么坑:它把 Scoped 仓储注册进单例,压测前一切正常,压测后数据串了
换来什么:同一接口从 52 分钟降到 38 分钟,返工集中在 DI 与查询两块

一句加分项:"我审 AI 代码先看 DI 生命周期和 EF 查询,因为这两块不报错,只在上量后出事。"

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。

① 自己写

  1. 写一个订单列表接口:分页 + 关键词筛选,只读查询 AsNoTracking,投影到 DTO,传 CancellationToken。
  2. 写一个后台任务服务,用 BackgroundService + IServiceScopeFactory 正确取 Scoped 依赖。

② 让 AI 写同一题

把第 2 题交给 AI,补一句 .NET 8,禁止把 Scoped 服务注入单例,必须处理取消与异常。

③ 对比要盯的点

检查面我的版本AI 版本差异说明
DI 生命周期
取消是否传递
查询条数与投影
异常是否被吞

④ 说取舍

  1. 它有没有把 Scoped 服务直接注入到单例?后果是什么?
  2. 它用 async void 了吗?异常会去哪?
  3. 它那版在 1 万条数据时会发多少条 SQL?

十、自测

  1. .NET 项目里 AI 最容易踩的四个隐式约定是什么?
  2. captive dependency 是怎么产生的?为什么压测前发现不了?
  3. 为什么"契约 → 失败路径测试 → 实现 → 生命周期审查"要分开?
  4. 五个检查点分别是什么?
  5. async void 与 .Result 各自的问题是什么?
  6. "查询闸"具体看哪些指标?

参考答案要点:DI 生命周期、可空引用、async 状态机、EF 延迟执行;Scoped 被单例持有,只在高并发时暴露;功能与审查混在一个 diff 里无法归因;DI、异步、EF 查询、可空与相等性、可观测与资源;异常无人接收、线程池饥饿;SQL 条数、线程池与连接池指标。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解