文章目录
-
- 那些让我想骂人的注释
- 那些救了我命的注释
- 所以到底该注释什么
-
- [1. 为什么这样做,而不是那样做](#1. 为什么这样做,而不是那样做)
- [2. 这里踩过什么坑](#2. 这里踩过什么坑)
- [3. 对调用者有什么隐含的要求](#3. 对调用者有什么隐含的要求)
- 关于"好代码不需要注释"
- 写注释的几个实际建议
- 最后
- 创作权保护
写了几年代码,关于注释这事,我的想法变了好几次。
刚入行的时候,恨不得每一行都写注释,生怕别人看不懂。后来看了几篇"好代码不需要注释"的文章,又走向另一个极端------觉得写注释是水平不够的表现。
现在回头看,两种都挺蠢的。
那些让我想骂人的注释
先说什么注释不该写。我真的在项目里见过这种:
csharp
// 定义一个整型变量i,初始值为0
int i = 0;
// 循环遍历列表
for (int j = 0; j < list.Count; j++)
{
// 判断是否大于10
if (list[j] > 10)
{
// i自增1
i++;
}
}
兄弟,我又不是不认识语法。
这种注释的问题不是"多",是全在说废话。它把你的注意力全部占满了,然后什么有用的信息都没传达。你读完注释和读完代码获得的信息量一模一样,那注释的意义在哪?
还有一种更要命的:
csharp
// 连接数据库并查询用户信息
var redis = ConnectionMultiplexer.Connect("localhost:6379");
var db = redis.GetDatabase();
var value = db.StringGet("user:1001");
注释说"连接数据库",代码连的是 Redis。这代码明显被改过,注释没跟着改。你信注释还是信代码?大多数人第一反应是信注释,然后就被带沟里了。
**过时注释比没有注释更危险。**因为没有注释你好歹会认真看代码,有了错误的注释你反而不看了。
那些救了我命的注释
说完烂注释,说说我真心感谢的注释。
有一次接手同事的项目,看到这么一段:
csharp
// 这里必须先断开再重连,不能复用旧连接
// 某些型号的设备固件有bug,复用TCP连接时第二次握手会返回脏数据
// 2024年6月在XX客户现场调了两天才发现的
// 换设备固件可以解决但客户不愿意停产升级,所以先这样处理
client.Close();
Thread.Sleep(50);
client = new TcpClient();
client.Connect(ip, port);
如果没有这段注释,我看到这段代码的第一反应一定是:"这人是不是不知道TCP连接可以复用?断开重连多浪费啊。"然后我会很自信地"优化"掉它,然后在客户现场翻车。
这种注释值多少钱?值我两天的出差费 + 客户的停产损失 + 我的头发。
再举一个:
csharp
private void SendCommand(byte[] data)
{
lock (_sendLock)
{
_stream.Write(data, 0, data.Length);
// 不能删这个Sleep
// 下位机MCU处理速度慢,连续发两条指令间隔<20ms时
// 第二条会被MCU当作第一条的后续数据拼接处理,导致指令解析错误
// 加到30ms是留了余量,实测25ms偶尔还是会出问题
Thread.Sleep(30);
}
}
Thread.Sleep 在代码审查里是最容易被质疑的东西之一。"为什么要Sleep?能不能去掉?"没有这段注释,总有一天会有人把它删了,然后设备开始抽风,然后排查三天发现是这30毫秒的问题。
所以到底该注释什么
我现在的理解是,代码本身能说清楚三件事:
- 做了什么------变量名、方法名、逻辑流程
- 怎么做的------算法、数据结构、调用关系
- 做了多少------循环次数、条件分支
但代码说不清楚另外三件事:
1. 为什么这样做,而不是那样做
csharp
public bool RecRealData(int stageID, byte[] aSend, int aRecLen, ref byte[] tmpRec)
{
// ReCon=1不做外层重连,只在内层重试3次
// 之前ReCon=3时,断线后一次调用会卡3*3*超时时间≈几十秒
// 上层100ms一轮循环,根本来不及更新UI上的连接状态
// 改成快速失败后,断线到界面显示<1秒
return ExecuteWithRetry(stageID, CMD_RECREALDATA, aSend,
"RecRealData", aRecLen, ref tmpRec, true);
}
不写这个注释,半年后的我看到 ReCon=1 会想:"是不是当时偷懒只重试一次?改成3次应该更稳吧。"然后断线检测又回到一分钟。
2. 这里踩过什么坑
csharp
private bool Read(int num, byte[] sendData, int recLen, out byte[] recData)
{
// ...
finally
{
// 每次失败都Close,不要改成"最后一次才Close"
// 原来是 if (num >= ReTry - 1) FClient.Close()
// 但TCP半开连接在对端断电时不会立刻检测到
// 不Close的话下次Open()会返回true(以为还连着),然后Write成功Read超时
// 这个循环会反复空转直到KeepAlive超时(默认好几十秒)
if (!rt) FClient.Close();
unLock();
}
}
这条注释就是血泪教训的记录。
3. 对调用者有什么隐含的要求
csharp
/// <summary>
/// 下发流程到MCU
/// </summary>
/// <remarks>
/// 调用前必须确保MCU处于停止状态,否则MCU会忽略本次下发且不报错
/// 下发成功不代表MCU已开始执行,需要另外调用CellControl启动
/// 本方法会阻塞当前线程约200-500ms
/// </remarks>
public bool FixSch(int stageID, byte[] aSend, out byte[] tmpSend, ref byte[] tmpRec)
不写这个,新来的同事直接调 FixSch,发现流程没生效,以为是通讯问题,查了一下午,最后发现是没有先停止MCU。
关于"好代码不需要注释"
这句话我现在这么理解:
**好代码不需要注释来解释"做了什么"。**如果你的代码需要注释来解释它在干嘛,说明代码本身写得不够清楚,应该先改代码:
csharp
// 差:代码看不懂,用注释补
// 判断是不是假电池或空位
if (Common.CheckFalseCellSpace(sn) ||
(!Common.CheckFalseCellSpace(sn) && Common.CheckFalseCellFake(sn)))
// 好:提取变量,代码自解释
bool isEmpty = Common.CheckFalseCellSpace(sn);
bool isFake = !isEmpty && Common.CheckFalseCellFake(sn);
if (isEmpty || isFake)
改完代码就不需要那行注释了。
**但好代码仍然需要注释来解释"为什么这样做"。**这一点再好的命名也做不到:
csharp
// 你能从方法名和参数名猜出下面这些信息吗?
public uMcuDispose(string ip, int port)
{
FClient = new TcpClientEx(ip, port)
{
// 局域网正常连接<10ms,150ms是为了断线时快速失败
// 原来默认2000ms,断线后光等连接就要2秒*重试次数
ConnectTimeout = 150,
SendTimeout = 300,
ReceiveTimeout = 300,
};
}
你不能。所以该写就得写。
写注释的几个实际建议
以下不是什么方法论,就是我踩坑总结出来的:
1. 写完一段代码,问自己:半年后有人看到这段,会不会手痒想改?
如果会,写个注释告诉他为什么不能改(或者在什么条件下可以改)。
csharp
// ConnectTimeout 设成150ms不是拍脑袋
// 实测:同一交换机下连接耗时<5ms,跨交换机<30ms
// 150ms已经留了5倍余量,再大就影响断线检测速度了
// 如果现场跨了路由器/VPN,可能需要调大到500ms
2. 改代码的时候看一眼附近有没有注释,有的话一起改。
做不到的话------我知道很多时候赶工期确实做不到------至少在注释前面加个 // TODO: 注释可能过时。真的,这比放着不管强一万倍。
3. catch { } 里面至少写一句为什么要吞异常。
csharp
// 差:沉默吞掉,没人知道这里可能出问题
catch { }
// 好:至少说明意图
catch
{
// 通讯异常在上层通过Connected状态处理,这里不需要单独报错
// 如果需要调试通讯问题,打开McuLogDetails开关看详细日志
}
裸 catch { } 是技术债里最难查的一种。等出了问题,异常被吞了,日志里什么都没有,你只能一行行加断点去猜。
4. 公共方法写 summary,不是因为专业,是因为实用。
csharp
/// <summary>通道暂停</summary>
public bool CellControl(int stageID, TMcuControl control, ...)
你可能觉得这是废话------方法名叫 CellControl,不就是通道控制吗?但当你的类里有十几个公共方法的时候,IDE的智能提示列表里看到 summary 和看不到是两种体验。这不是写给阅读源码的人看的,是写给调用你的方法的人在智能提示里扫一眼的。
5. TODO/HACK 注释要带上日期和原因。
csharp
// 差:
// TODO: 优化这里
// 好:
// TODO(2026-04): 这里每次刷新都new一个byte[],GC压力大
// 等通道数>256的项目立项后再改成对象池,当前项目最多128通道,问题不大
没有日期和原因的 TODO 会永远留在代码里,变成一个谁也不敢动的"历史遗迹"。有了日期和原因,后来人至少知道这个TODO是什么时候留的、为什么留的、什么条件下该处理。
最后
我不觉得注释有什么"最佳实践"或者"行业标准"。每个项目的情况不一样,每个团队的水平不一样,每个人半年后忘事的速度也不一样。
我只知道一件事:
我翻自己三个月前写的代码,经常看不懂当时在想什么。那些留了注释的地方,我能在10秒内捡起来;没留注释的地方,我得重新读一遍代码,有时候还要翻聊天记录和会议纪要才能想起来当初为什么这么写。
所以现在的习惯很简单:写完一段代码,如果我觉得三个月后的自己可能会困惑,就加一行注释。
不多不少,就这样。
创作权保护
本文由 [ leonkay ] 学习总结编写,水平有限,内容仅供参考,作为个人记录使用。若有疏漏,请不吝赐教。版权归作者所有,未经授权,禁止转载、摘编或以其他方式使用本文内容。如需合作或转载本文,请联系作者获得授权。