2461 字
约 8 分钟
14
S24-A22 · AI 与代码风格:统一了格式,却不是同一个"写法"

一个团队引入 AI 编码后,spotless 与 checkstyle 全部通过——格式完全统一。而半年后评审变得很慢:同一个需求,五个人的 AI 给出五种"结构"(有的用 Stream、有的用 for 循环、有的拆成三个小方法、有的写成一个长方法),而格式检查对此一无所知。

代码风格有两层:格式(缩进、命名、括号)与写法(结构、抽象、错误处理)。工具能管前者,这个分层解释了"为什么格式全绿却难读":格式只影响"看第一眼",写法影响"读懂一段要走多少步"——而后者才是评审耗时的主要来源。
而后者才是"读起来累不累"的主要来源。


1. 两层的可自动化程度

层 能否自动化
格式(缩进/换行/命名) 能(formatter + linter)
写法(结构/抽象层级) 难(需"约定 + 评审")
错误处理风格 中(可用检查拦一部分)
注释风格 中(可检查"有无 doxygen")

第二项是"AI 放大了差异"的地方:格式工具把它压平了,而"结构"的差异被保留并放大第二项的"放大机制"是"AI 一次生成一整块":人工写代码时"结构"随讨论逐步收敛,而 AI 一次给出"成品",于是"它选的抽象层级"直接成为结果。
——因为 AI 会"按自己的偏好"选结构。

2. 三种"写法差异"的具体表现

差异 例子
控制流 Stream vs for
抽象层级 一个长方法 vs 三个小方法
错误处理 抛异常 vs 返回 Optional
数据形状 DTO vs Map

第四项的"隐藏成本"在"后续修改":Map 的键没有类型,改字段名时编译器不报错——于是"改名"变成了"运行时才发现"(与"安全重命名"的坑同源)。
第四项最容易被忽略:"传 Map"与"传 DTO"在编译期都不报错,而它们"后续怎么改"完全不同。

3. "写法约定"该怎么写才有效

写法 效果
"尽量用 Stream" 弱("尽量"无边界)
"集合转换用 Stream;含副作用用 for" 强(有条件)
"方法不超过 30 行" 中(可检查)
给出"反例 + 正例" 最强

第二项是"可判定"的写法:它给出"什么时候用哪个"的判据第二项的"检验方式"很实用:把约定给两个不同的人看,问"这段代码该用哪个"——答案一致,说明约定可判定;不一致,说明它还是"偏好"。
,于是"选择"变成"按条件判断",而不是"按偏好"。

第四项的"原因"是"示例是"具体的",规则是"抽象的"":具体的东西可以被"照抄",抽象的东西要"解释"——而解释会引入歧义。
第四项对 AI 特别有效:AI 对"示例"的遵循度高于"规则描述"——于是"一个反例 + 一个正例"比"三条规则"更能统一写法。

// 一条有效的写法约定(含条件与示例)
规则:集合的"纯转换"用 Stream;"含副作用(日志/IO/状态变更)"用 for。
正例:list.stream().map(User::name).toList()      // 纯转换
反例:list.forEach(u -> { log.info(...); repo.save(u); })  // 副作用
判据:循环体里有没有"不只是算值"的动作

4. AI 为什么会"各有偏好"

原因 机制
训练语料多样 见过多种写法
提示未约束 "写一个方法"没有结构要求
上下文不同 参考了文件里已有的写法
模型版本不同 偏好随版本变

第三项是"可利用"的:**如果文件里已有"统一写法",AI 会"照着写"**第三项的"实践"很直接:把"参考实现"的文件一起放进上下文,并说明"照这个文件的写法"——于是"风格"通过"示例"传递,而不是通过"规则"。
——于是"让第一个文件写对"比"事后统一"便宜得多。

第四项的"量化"可以很轻:每月抽 20 个 AI 生成的 PR,统计"用了 Stream / 用了 for / 拆成几个方法"——分布变化就是"漂移"。
第四项说明"统一"是"持续动作":换模型版本会带来写法漂移,所以要"定期抽样对比"。

"写法约定"应当只写"高频、可判定"的几条,否则没人记得住:

// 一份"够用"的写法约定(五条,都带判据)
1 集合纯转换 → Stream;含副作用 → for
2 一个方法只做一件事;超过 30 行拆(含判据:能否用一句话命名)
3 对外返回值用 Optional/DTO,不返回 null/Map
4 异常只抛"业务异常 + 参数",不抛裸 RuntimeException
5 IO 不在循环里(收集后批量)
// 判据:每条都能用"一段代码该不该这样写"来回答

5. 三种"统一写法"的手段

手段 成本
约定文档(含示例) 低(但不执行)
静态检查(自定义规则) 中(能拦一部分)
代码模板/骨架(脚手架) 中高(最有效)

第三项最有效:"新代码从一个骨架开始"第三项的"落地"是"模板 + 生成器":new-service 命令生成"controller/service/repo + 测试"的骨架,AI 只填"业务部分"——于是"结构"不由它决定。
,于是"结构"被固定,AI 只能"填内容"。

第二项能拦"可判定的部分":"方法超过 30 行"、"catch 块为空"、"循环里有 IO" 这类规则可以做。

// 自定义静态规则(示例:拦"循环里的 IO")
- id: io-in-loop
  pattern: for (...) { ... repo.save(...) ... }
  message: "循环内做 IO:应收集后批量写入"
  severity: warning
// 收益:把"写法约定"变成"可执行检查";代价:规则要维护、可能误报

"评审意见"可以按"是否可判定"分类,从而判断"标准够不够清":

// 三类评审意见(比例决定"标准质量")
A 逻辑/边界错(应当占多数)        → 说明评审在看"对错"
B 写法偏好("我觉得用 for 更好")  → 说明标准不清
C 格式(缩进/命名)                → 应当为 0(工具管)
// 判据:B 类占比 > 30% → 先补"判据",而不是"多评审"

6. 排错:五种"风格统一了但评审变慢"

现象 成因 处理方向
格式全绿但读着累 写法差异 加"写法约定 + 示例"
同一需求五种实现 无结构约束 模板/骨架
换模型后风格变了 版本漂移 定期抽样
约定没人遵守 无检查 静态规则
评审都是"风格意见" 无共同标准 先定判据

第五项的"修法"是"把争议点写成判据":每次争论后记一句"这种情况用 X",于是"下一次同类争论"有依据——而这份记录就是"团队约定"的雏形。
第五项是"评审质量"问题:评审里一半意见是"我觉得这样更好",说明"标准不清"——而它会让"该关注逻辑的评审"变成"风格争论"。

最后一个"粒度"建议:把"生成粒度"缩小到"一个方法/一个类",写法差异会自然收敛。

// 三种生成粒度与"写法一致性"
整块功能("实现订单模块"):结构由模型决定 → 差异最大
一个类("实现 OrderService"):结构半定 → 差异中
一个方法("实现这个方法体"):结构已定 → 差异最小
// 判据:团队是否已能"用一句话说清结构"

7. 边界:什么情况不必统一写法

场景 为什么 做法
单人项目 无协作成本 不统一
一次性脚本 不长期 不统一
强性能代码(手写优化) 结构由性能决定 例外
已有成熟规范(大厂风格) 跟随即可 跟随

判断口径一句话:"同一个需求,不同人写出来的结构差异,会不会让评审与维护变慢"。最后一条与"AI 使用方式"有关的边界:"让 AI 一次生成整块"与"让它填一个函数",对写法统一的影响差别很大。所以"统一写法"的一条隐含实践是"缩小生成粒度"。
会(多人协作、长期维护),就必须统一"写法";不会(单人、短期),格式统一就够了。


8. 自测

  1. 代码风格的两层里,哪一层工具能管、哪一层管不了?为什么 AI 会"放大"后者?
  2. "尽量用 Stream"为什么是弱约定?"含副作用用 for"为什么强?
  3. 为什么"反例 + 正例"对 AI 特别有效?
  4. "上下文不同"为什么是"可利用"的原因?它带来什么实践建议?
  5. 三种统一手段里,哪一种最有效?为什么"骨架"比"约定文档"有效?
  6. 判断"是否需要统一写法"的那个问题是什么?
S24-A22 · AI 与代码风格:统一了格式,却不是同一个"写法"
https://www.clxhxhhr.top/posts/6607/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。