0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

函数的设计功能是什么

马哥Linux运维 来源:马哥Linux运维 作者:马哥Linux运维 2022-11-01 10:19 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

每隔一段时间,网上总会突然出现一些令人讨厌的帖子,其观点是:不应该为代码写注释,它存在的唯一原因是因为代码本身不足够好。对于这些论点,我完全不能苟同。

烂代码

他们的观点也不完全是错误的。没有人能说自己的代码足够好。代码本身也会慢慢变坏。你知道什么时候代码腐烂得最厉害吗?当你六个月没有碰这些代码的时候!

当回过头再读的时候,你会非常好奇:“这个作者到底是怎么想的?”(于是,使用 Git blame 来查看历史记录,没想到代码竟然是自己写的,因为这是你的代码。)

反对注释者的论点是:需要注释的唯一原因是你的代码不够“清晰”。如果代码重构、命名和组织地更好,那就不需要这些注释了。

今天,当整个项目和问题空间都装在你的脑袋里的时候,你自然会觉得代码是干净、清晰和优雅的。但是,当六个月后,又或是 CTO 刚好在生产系统上突然发现一个非常严重的 bug,在主管紧盯的情况下,某个可怜的家伙不得不去调试你的代码的时候,这些代码可能对你已经有些模糊了。

你无比熟悉的一段代码,尝试去理解其他人在什么场景下不能理解,这是一种非常难以掌握的技能。不过,它具有无可估量的价值,几乎和一次就能把代码写到位的能力一样重要。在工业界中,基本上没有人是独行侠。即使真地在独自写代码,你也会遗忘代码这么写的缘由或者昨天深夜“工程代码”核心部分的确切目的。未来一旦你离职,接替你的人不得不去理解每一个仅藏在你的脑袋里的小偏好和习惯。

所以,写上一个即使在现在看来过于浅显的注释也不是一个坏事情。有时候,它甚至会带来巨大的帮助。

无注释经常导致代码更难以理解

某些人声称:移除注释将会使代码变得更好,因为这迫使你编写更清晰的代码。我对此亦不以为然,因为我不认为有人会实际写上一些次佳的代码,并且写上一些注释来解释这种行为(除了// TODO: 这是一个临时的解决方法,我会稍后修正之外)。我们都会写出在各种外部条件(通常是时间)下自认为最好的代码。

为去除注释而重构代码的问题在于,这种努力往往事与愿违,会产出更坏的代码。典型例子是重构一行复杂的代码,将之提取到一个独立的函数中,并取一个望文生义的名字。这个行为看上去很棒,但是,现在你为阅读代码的人带来了一个上下文切换点。替代真实代码的是一个函数调用,于是,他们不得不滚动到函数定义的地方,记住和对照函数声明和调用的参数,并且将函数返回值代入到调用的地方。

另外,清晰的函数名仅仅能够提供非常短小的注释。任何需要多余一小段短语的注释无法(或者不应该)概括到一个函数名中。因此,你最终会得到一个其上有注释的函数。

的确,一个非常短小的函数都可能导致困惑和更复杂的代码。如果看到这样的函数,我会去搜索这个函数在哪些地方被调用。如果只有一个地方,我就会去思考,这是一个确实封装了全局逻辑的通用代码块呢(譬如 NameToUserID),还是,这个函数严重依赖调用端的特定状态和实现,并且不能在其他地方正确工作。随着把这些代码提取一个函数里面,你本质上在其余的代码库中暴露了这些实现细节,这么草率的做决定是不合适的。即使你知道这个函数其他人不应该调用,其他人还会在某些地方调用它,即便这些地方不合适这么做。

小函数的相关问题在 Cindy Sridharan 在 medium 网站上的帖子[1]中有更加详细的阐述。

我们甚至可以深入讨论长短变量名的比较和权衡,但是就此打住吧,一般你不可能接受更长的变量名了。除非你的变量名就是你想写的完整的注释,否则你还是会丢失信息而不得不添加到注释中。我认为我们可以达成一致:usernameStrippedOfSpacesWithDotCSVExtension 是一个可怕的变量名称。

我不是说我们不应该提炼代码,让它们更加清晰和优雅。绝对要这么做!这是一个杰出开发人员的特征。但是,代码清晰性和有注释是正交的,撰写良好的注释也是杰出开发人员的特征。

没有坏注释

在这些讨论中给出的坏注释的例子都是些小错误,除了那些启蒙编程课程外,在实际工作中几乎不会碰到。

// 实例化一个错误对象
var err error

不错,这个注释很清楚,但不是非常有用。不过同时,它实际上也没有什么坏处。

在浏览代码时,虽然有些不待见,但也很容易被忽略。如果开发者能够在其中包含一个有用的注释,能够节省我数小时键盘工作时间的话,我宁愿看成百这样的简单注释,而不是没注释。

我非常确信,不会有任何代码会说“伙计,这段代码非常容易理解,所以不需要提供任何注释。” 实际情况恰恰完全相反。

实际上,我找到了一些严重缺失注释的代码 - Go 标准库。它的代码非常精良,但在很多情况下,如果在读取代码前对其功能没有深刻理解,那么理解他们为什么这么设计将是个挑战。如果能加一些注释,用于解释代码的逻辑和设计意图,将使 Go 标准库更加容易阅读。在这篇文章中,我主要讨论实现代码里的注释,而不是通常的公开函数的文档注释(通常情况下,它们也是非常棒的)。

任何注释胜过无注释

另外一个反注释者喜欢拿出来的例子,可以用下面的简洁有力的图片来展示(证明其论点):

哈,极好笑的,有人更换了瓶子里面的东西但是没有更新外面的标签

但是,这是 20 年前的问题了,当时通常不进行代码审查。不过,现在代码审查已经非常普遍了。如果检查注释和实现是否匹配不是你们代码审核流程的一部分,那么最好检查一下你们的代码审核流程。

这不是说不会犯错误,实际上我昨天刚提交了一个“注释和实现不一致”的 bug。类似“无注释比错误注释好”的言论初听起来是正确的,然而,当你认识到如果没有注释,开发人员猜错代码的功能比错误注释的出现的概率高的多的时候,你会改变你的看法。

即使这种情况真的发生,代码被修改了,你依然可以获取有价值的信息:代码以前的用途。修改仅仅和原先有些许不同罢了,它依旧完成基本相同的功能。为了版本控制和向后兼容,同一个函数在不改变名称和签名的情况下,在功能上发生剧烈变化的频率有多少?基本上非常少。

就拿我昨天发现的 bug 来说,我们调用 client.SetKeepAlive(60)。而 SetKeepAlive 函数的注释是 “SetKeepAlive 在发送 PING 请求之前,客户端需要等待指定数量的时间(以秒为单位)”。看上去很棒,不是吗?知道我注意到 SetKeepAlive 的参数是 time.Duration。

如果没有其他指定的单位,60 这个整数将使用 Go 的 duration 的缺省单位纳秒。哎,某人更新了该函数,使用 Duration 类型来替换 Int。有趣的是,它仍然向下取整到了最接近的秒数,所以注释不是不正确,只是有些误导罢了。

为什么?

最重要的注释是为什么要注释。为什么代码是按照设计来执行的?为什么这个 ID 需要小于 24 个字符?为什么要在 Linux 下面隐藏这个选项?诸如此类。这些问题为什么重要的原因是你无法从代码中提炼出来。这些注释总结了开发者获得的经验教训,商业或系统层面的限制条件等,它们是价值无量的,并且几乎无法从其他途径获得(例如,函数取名应该反映函数做什么而不是为什么)。

那些用于说明代码功能的注释往往不是特别有用的,因为如果拥有足够的时间和努力,你总能够理解代码的功能。本质上,通过函数定义,代码往往会告诉你它的具体功能,但这不意味着你不应该写任何注释。确实应该力争写出最清晰简洁的代码,但是注释不需要任何额外的运行时开销,如果你觉得有人会错误理解一些代码或者理解上有困难,应该写上一些注释。至少,这个会节省他们半个小时来理解你的代码,这些注释也会在很大程度上帮助他们避免错误地修改或使用你的代码,从而导致 bug 的产生。

测试

一些人认为函数的功能测试案例就相当于文档。某种程度上说,确实是这样的。但是,在我的效率文档表中,它的优先级非常低。为什么呢?因为它们极其精确而且琐碎,仅仅覆盖了功能的很少一部分。每一个测试仅确切地测试一个特定的输入和与之相配的输出。任何超过一个简单函数的情况,你很可能需要一大串代码来构建输入和输出。

对于大多数编码而言,描述一个函数的主要功能比写代码去完整测试要容易的多。

很多时候我的测试代码行数倍于函数实现本身,然而文档注释仅仅需要寥寥几行而已。

此外,测试仅仅解释了函数的功能。函数的设计功能是什么?它们不能解释为什么,但是就像前面提到的,设计目的和意图总是更重要的。

你确实应当测试你的代码,通过一些边界测试案例,测试对于判定代码在边界条件下是否能够正常工作非常有用。但是一般而言,如果到了必须通过阅读测试案例来理解代码的地步的话,那么已经是一个危险信号,告诉我们需要去编写更多更好的注释了。

结论

除了一些非常简单的例子以外, 有用注释和无用注释的边界是非常难于去发现的。

所以,我宁愿人们站在多写注释的一方。你无法知道下一个可能阅读你代码的人是谁,所以能帮助他们的是尽你所能写上一大堆的注释。尽量写到你认为太多了,然后再多写一些,这个数量估计就正好了。

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    4406

    浏览量

    66838
  • 代码
    +关注

    关注

    30

    文章

    4941

    浏览量

    73148
  • BUG
    BUG
    +关注

    关注

    0

    文章

    156

    浏览量

    16225

原文标题:浅谈 Go 语言代码注释问题

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    LUA例程-常用的回调函数使用说明

    详细说明LUA脚本函数功能和对应的应用实例。对于LUA脚本编程有很大的帮助和提高技能。
    发表于 11-24 16:43 0次下载

    LuatOS MCU核心库全接触:新手操作与功能测试攻略!

    开启芯片级开发实战。 一、MCU核心库函数功能 MCU核心库是LuatOS中封装MCU一些特殊操作的核心模块,提供了对MCU底层功能的访问和控制能力,是开发高级功能和系统调试的重要工具
    的头像 发表于 11-12 14:30 141次阅读
    LuatOS MCU核心库全接触:新手操作与<b class='flag-5'>功能</b>测试攻略!

    新手必备:LuatOS MCU核心库功能测试与实践指南

    各类开发需求。 一、MCU核心库函数功能 MCU核心库是LuatOS中封装MCU一些特殊操作的核心模块,提供了对MCU底层功能的访问和控制能力,是开发高级功能和系统调试的重要工具。 详
    的头像 发表于 11-12 14:26 149次阅读
    新手必备:LuatOS MCU核心库<b class='flag-5'>功能</b>测试与实践指南

    蜂鸟E203在黑金XC7A200T型FPGA上点亮LED并实现流水灯

    ) 3、外部中断的相关函数配置。 4、根据自己的功能需求写好中断函数。 (注意:中断程序中最后需要对中断等待标志寄存器写入1,从而结束中断操作) 5、在main
    发表于 10-31 09:04

    _timer_start()函数功能虽然正常,但实现是否未达预期,怎么处理?

    |= RT_TIMER_FLAG_ACTIVATED; return RT_EOK; } 虽然功能都正常,但在双层循环中查找新节点插入节点时,代码中没有利用跳表的优势呀。如果将跳表设置为多层的话,里层的循环
    发表于 10-14 07:34

    灵活高效ZBUFF — C内存数据操作库:优化内存管理的利器

    流程,助力项目实现资源利用率与运行速度的双重突破。 一、ZBUFF核心库常用函数 如果小伙伴们此前没有接触过C语言或者不精通C语言,把ZBUFF核心库当成新库来学习即可,建议先从熟悉相关函数功能及其参数意义入手。 感兴趣的朋友
    的头像 发表于 08-14 18:01 519次阅读
    灵活高效ZBUFF — C内存数据操作库:优化内存管理的利器

    灵活高效双引擎驱动:ZBUFF让C语言内存操作更智能!

    高性能系统。 一、ZBUFF核心库常用函数 如果小伙伴们此前没有接触过C语言或者不精通C语言,把ZBUFF核心库当成新库来学习即可,建议先从熟悉相关函数功能及其参数意义入手。 感兴趣的朋友可查看下方API文档。 最新核心库AP
    的头像 发表于 08-11 13:27 439次阅读
    灵活高效双引擎驱动:ZBUFF让C语言内存操作更智能!

    详解hal_entry入口函数

    当使用RTOS时,程序从main函数开始进行线程调度;当没有使用RTOS时,C语言程序的入口函数main函数调用了hal_entry函数。由于我们新建的工程是没有选用RTOS的,因此,
    的头像 发表于 07-25 15:34 1662次阅读

    itop-3568开发板机器视觉opencv开发手册-图像绘制-画线

    函数功能: 绘制一条直线。 函数原型: cv2.line(img,pt1,pt2,color,thickness=None,lineType=None,shift=None) 参数定义: img
    发表于 06-04 10:38

    详解RTOS中的Hook函数

    Hook函数是RTOS中的一个关键特性,通过该函数,用户可以增强对任务管理的控制,定义系统行为。
    的头像 发表于 03-24 16:14 838次阅读

    基于瑞萨RA0E1开发板的ThreadX实时操作系统串口回显测试

    感谢立创提供测试机会,学到RA0及瑞萨MCU的相关使用。使用瑞萨FSP库和配置工具大大简化了开发过程。掌握相关函数功能,如打开串口、设置串口参数、读取和写入串口数据等。
    的头像 发表于 02-25 09:27 921次阅读
    基于瑞萨RA0E1开发板的ThreadX实时操作系统串口回显测试

    迅为RK3568开发板篇Openharmony配置HDF控制UART-UART 接口运作机制

    UART 设备传输模式,关闭 UART 设备的接口。  核心层主要提供 UART 控制器的创建,移除以及管理的能力,通过钩子函数与适配层交互。  适配层主要是将钩子函数功能实例化,实现具体的
    发表于 02-19 10:41

    西门子TIA Portal中函数FC和函数块FB的相互转换

    描述 本文将介绍在西门子 TIA Portal 中使用 Add-In 插件实现函数 FC 和函数块 FB 的相互转换的方法和步骤。 第1步: 添加 PLC 设备。 选择西门子 CPU 1214C
    的头像 发表于 01-15 10:07 3037次阅读
    西门子TIA Portal中<b class='flag-5'>函数</b>FC和<b class='flag-5'>函数</b>块FB的相互转换

    用51单片机控制TLV5618进行数模转换,想利用+1.024内部参考电压,芯片REF引脚要怎么接,是悬空还是接地?

    (uint Dignum) // 函数功能 :进行DA转换 // 入口参数 :Dignum:根据说明设置转化数据.头四位为特殊位用于选择转化方式, // 以及用于通道选择.请自行设置.后12位为需要转换的值
    发表于 01-08 07:56

    各类Modbus功能接口函数详解

    函数对应于功能码01(0x01)读取线圈/离散量输出状态(Read CoilStatus/DOs),其中,所读取的值存放于参数uint8_t * dest指向的数组空间因此dest指向的空间必须足够大,其大小至少为nb * sizeof(uint8_t)个字节。
    的头像 发表于 12-11 17:12 3440次阅读
    各类Modbus<b class='flag-5'>功能</b>接口<b class='flag-5'>函数</b>详解