本章旨在帮助各位读者们去了解应该写什么样子的注释,你可能以为注释的目的就是解释代码做了什么,没错,但这只是其中一部分。
注释的目的
尽量传递信息给读者,使其对代码的熟悉程度和作者一致。
当你写代码的时候,脑海里有很多有价值的信息,你没有选择记录下来;
当其他人阅读你的代码的时候,这些信息就丢失了---它们看到的只有代码。
因此,本章主要围绕以下三点展开:
- 仅当需要的时候,才使用注释
- 记下有价值的信息
- 站在读者的角度,去想象他们需要什么
仅当需要的时候,才使用注释
1 如果代码本身可以快速推断出其意义,就没必要写注释。
注释会分散读者理解代码的注意力,会占用屏幕空间,因此,注释是要有存在价值的。
比如下面的代码里面的注释对于读者而言,是没有价值的。
/// <summary> /// 日期工具类,提供日期的一系列函数 /// </summary> class DateHelper { /// <summary> /// 私有日期变量 /// </summary> private DateTime _currenDateTime = DateTime.Now; /// <summary> /// 默认构造器 /// </summary> public DateHelper() { } /// <summary> /// 获取日期 /// </summary> /// <returns></returns> public DateTime GetTime() { return _currenDateTime; } /// <summary> /// 设置日期 /// </summary> /// <param name="newdate"></param> public void SetTime(DateTime newdate) { _currenDateTime = newdate; } }
2 注释不是复述一遍代码
如果注释仅仅只是复述了一遍代码字面上的意思,那么它对读者的价值就大大降低,这种情况,要么删掉注释;要么让注释传递更多的信息
我们来看个例子:
//使用指定名称在树指定的深度查找节点 Node FindNodeInTheTree(Tree tree, string nodeName, int searchDepth);
很显然,这个注释就属于“复述型”,要么删除它
如果查找方法还有其他细节的,你可以选择完善注释,比如下面的修改方案
//通过名称查找指定的节点,否则返回null //如果searchDepth<=0,那么只在tree根目录查找 Node FindNodeInTheTree(Tree tree, string nodeName, int searchDepth);
3 注释不应该拿来粉饰
注释不应该拿来粉饰不友好的命名,应该使用重构工具来重命名一个友好的名字。
比如,我们有个方法,在公司人员离职的时候调用
// 当员工离职的时候,调用本方法 // 这个方法不会从数据库物理删除该用户所有数据 // 只是将用户的状态设置为禁用 bool DeleteUser(Guid userGuid);
这个时候,我们与其拿注释来粉饰这个糟糕的方法名称,还不如重构它
bool DisableUserWhenDimission(Guid userGuid);
好的代码 》 坏的代码+好的注释
记下有价值的信息
1 加入你的心得
1.1 探过的路,不必再探
比如:有个方法需要用到排序。
//在测试中,我们比较了快速排序、选择排序、冒泡排序 //最终我们选择快速排序,因为对比发现,快速排序速度最快,占用内存最小
1.2 踩过的坑,不要再踩
比如我们系统中,引入了第三方插件,但是有个BUG
//这个插件在随机创建用户名的时候 //极小情况下,会出现用户名重复的情况 //已经反馈给作者,等待作者修改,不是系统的BUG
2 为代码中的瑕疵注释
在敏捷开发里面,更新非常频繁,代码在不断的成长,最终演变成最终版。但是在这个迭代的过程中,代码肯定会存在瑕疵,我们就可以通过注释,将这些瑕疵记录下来。
这种注释给读者带来对代码质量和当前状态的宝贵理解,甚至可能会给他们指出如何改进代码的方向。
//TODO 我还没有处理完的事情 //FIXME 已知的有异常的代码 //HACK 有待优化的低劣方式 //XXX 危险! 这里有问题
3 给常量加注释
简单点讲,就是回答读者:为啥这个常量取这个值。
3.1 常量调整指南
//线程的数量:经过测试,发现如果该值<= 2*cpu核的时候,系统表现良好 const int THREAD_COUNT = 6;
3.2 不要乱动
//人类的移动速度:经过多次调整,发现在3.14速度下,人类的移动速度最自然 private const float PEOPLE_MOVE_SPEED = 3.14f;
3.3 补充说明
//人类体重的限制,额,正常人不会超过这个重量吧 const int MAX_HUMAN_WEIGHT = 500;
有些常量如果本身名称就足够清晰,那么就不需要注释,比如(PI=3.1415;MINUTES_PER_DAY=1440)
4 站在读者的角度
这个“读者”可以是不熟悉项目的人,也可以使若干个月之后我们。想象下,我们平时写的代码,对于刚接手的外人来讲,是什么样子?是泥淖还是风景?
4.1 意料之中的提问
很多网站上都专门开辟了常见问题(F&Q)专栏,对读者80%的常见问题,进行统一回答,省时省力。
因此,你觉得作者在读我们的代码的时候,遇到某些模块,会有“什么?为什么要写成这样?”的问题的时候,就可以给这个问题加上注释。
比如下面的c++代码,我们要清空一个数组,读者看到Clear方法可能会疑惑
Struct Recoder{ vector<float> data; //.... void Clear(){ //清空数组为啥不用data.clear()? vector<float>().swap(data); } }
所以我们要做的是回答读者,为啥不使用data.clear()这个常用清空数组的方法?
//实际上,只有这样才会强制让data真正的把内存归还给内存分配器 //详情参阅:“STL swap trick” vector<float>().swap(data);
4.2 公布缺陷
当为一个函数或者类的写文档的时候,可以问自己“这段代码有啥出乎意料的地方,会不会被误用?”。
也就是说,你要“未雨绸缪”,预料到别人使用你代码的时候,遇到的问题。
比如一个向用户手机发送验证码的函数:
void SendMessageToPhone(string to,string subject,string body);
但是因为短信提供商的问题,这个方法有可能会延迟几个小时(不是实时的),
所以,我们有必要给这个函数打上注释
//给用户手机发送短信,因为运营商的关系,可能会造成延时,(我们试过1个多小时后才收到短信。。擦) void SendMessageToPhone(string to,string subject,string body);
4.3 全局观注释
对于团队的新成员来讲,最难以理解的事情之一就是理解“全局观”---类之间如何交互啊、数据如何在整个系统中流动啊、项目入口点在哪里啊。。。balabalabala
设计系统的人常常忘记给这些点加上注释,因为他们觉得这些很容易理解,以至于不需要注释。
想象下这样的场景:“团队来了个新人,他坐在你旁边,而你的任务是让他快速熟悉代码,进入到开发的角色中。”
因此,你可能会带着他遨游现有的代码库,指着某些文件、代码侃侃而谈:
“这段代码把我们的业务逻辑和数据库黏在一起,任何应用层的代码都不应该和他交互。”
“这个类看上去很复杂,实际上,它只是一个巧妙的缓存,它对系统中其他部分一无所知。”
“页面的事件执行顺序是a->b->c->d,我们之所以要这样设计,是因为balabalaba。”
“这个类包含一些辅助的函数,为我们的网站后台管理提供了便利的接口,用来处理用户的权限问题,比如登陆、管理”
相比阅读源码这样的交谈是不是让新人获益良多?那为什么我们不把他们整理下,写到注释中呢? “好记性不如烂笔头”
4.4 总结性注释
方法内部如果逻辑比较多,或者比较复杂。除了第一步进行重构,第二步我们可以给对应的代码块写上注释
对这个函数所做的事情做个总结,因此读者在深入了解代码前,对这个代码干啥用的已经了然于胸。
void ShowCustomBoughtItems(string userName) { //1 根据用户名从数据库中查找出该用户ID //2 根据用户ID从商品-用户关联表里找出商品ID列表 //3 根据商品ID关联商品表,获取对应的商品详情 //4 列出这些详情 }
你可以做任何帮助读者更理解代码的事情,而不必纠结于注释的职责。“做什么”、“怎么做”、“为什么这样做”都可以拿来活用。