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

    文章

    4328

    浏览量

    62575
  • 代码
    +关注

    关注

    30

    文章

    4780

    浏览量

    68531
  • BUG
    BUG
    +关注

    关注

    0

    文章

    155

    浏览量

    15665

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

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

收藏 人收藏

    评论

    相关推荐

    各类Modbus功能接口函数详解

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

    SUMIF函数与SUMIFS函数的区别

    SUMIF函数和SUMIFS函数都是Excel中用于条件求和的函数,它们可以帮助用户根据特定的条件对数据进行求和。尽管它们的基本功能相似,但在使用场景和
    的头像 发表于 10-30 09:51 1033次阅读

    为什么按键后串口收到很多组相同的数据?

    名 : uart_init 函数功能 : 串口通信中断配置函数,通过设置TH和TL即可确定定时时间 输 入 : baud:波特率对应的TH、TL装载值 输 出: 无
    发表于 09-28 21:19

    【CH32V208】2、体验systick

    ); } 此函数功能为输出一个计算值,并在串口中输出,以便观察 5、主程序中,我们传一个配置溢出值为系统时钟-1即1秒种进入一次中断。 【实验现象】 下载到开发板后,打开串口终端,可以成功实现即定功能
    发表于 07-31 09:37

    tcpip_adapter_start_api 函数功能是什么?

    , msg->mac, msg->ip_info); } 请问 1. tcpip_adapter_start_api函数功能是什么??或者说tcpip_adapter_start中调用
    发表于 06-26 07:08

    接口测试的工具有哪些种类

    单元测试框架 单元测试框架主要用于测试单个模块或函数功能。虽然它们主要用于开发阶段,但也可以用于接口测试。 1.1 JUnit (Java) JUnit 是 Java 语言的单元测试框架,支持自动化测试,可以测试 Java 编写的接口。 1.2 NUnit (C#)
    的头像 发表于 05-30 15:07 702次阅读

    求助,关于在STM8S用FOR遇到的疑问求解

    我用了一块STM8F103F3P,TSSOP20封装的最小系统板,直接连接WS2811的灯条,用数组来存贮要发送的数据,发送是这样写的: /*********************** 函数功能
    发表于 05-16 07:30

    STM32F3执行函数的时候进入HardFault_Handler死循环,怎么处理?

    + (uint32_t)(dataRx1 << 16)); return *Result; } 执行这个函数的时候进入HardFault_Handler死循环,求助怎么处理?函数功能是从某个芯片中读取两个字,网上
    发表于 05-13 07:35

    STM32片上flash能否读取正在运行的代码段内容?

    如题 小弟想请教下大家 STM32片上flash的读取问题。举个例子: 函数功能是 读取片上flash 0x0800_1000~0x0800_2000的存储内容 函数存储在flash的 0x0800_1000~0x0800_2
    发表于 04-16 07:22

    求助,关于STM32F3系列启动时SWD的配置问题求解

    求助:小弟现在使用STM32F373RCT6这款芯片,原来使用F103的,由于IO资源使用比较紧凑,基本没什么剩余IO,所以在system_stm32f1xx.c中增加了一个函数
    发表于 04-09 06:12

    现场可编程门阵列的基本结构和优缺点

    主要由查找表(LUT)和D触发器(DFF)等逻辑电路构成。查找表用于实现逻辑函数功能,而D触发器则用于存储逻辑状态。嵌入式块RAM提供了额外的存储空间,布线资源则负责各个逻辑单元之间的连接。
    的头像 发表于 03-27 14:49 604次阅读

    使用CubeMX配置STM32010C6T6的LPUART1外设,调用串口发送和接收函数均不能收发数据怎么解决?

    串口初始化文件中将RX引脚模式更改为输入类型同样不能接收到数据 在CubeMX中更换MCU型号为F103系列并配置串口,则RX引脚默认为输入模式,并且生成的工程调用串口收发函数功能正常 这种现象是不是CubeMX软件导致的问题呢,应该如何解决(生成的工程默认没有使能串口,
    发表于 03-19 07:48

    【RISC-V开放架构设计之道|阅读体验】汇编语言和扩展指令集

    函数可访问的位置; 2)跳转到函数入口(使用RV32I的jal指令); 3)获取函数所需的局部存储资源,按需保存寄存器; 4)执行函数功能
    发表于 02-03 13:29

    ADT7310读取数据波动较大是什么原因导致的?

    PORTB= ~BIT(CS) //片选关 /* 函数功能: SPI初始化 */ void Spi_Init(void) { //使能SPI,先发送MSB //主机模式,SPI工作在模式0
    发表于 01-02 07:37

    ADXL355为什么不能进入测量模式?

    抓取到的数据波形,以及代码: 这里是抓取到的读取ID寄存器的数据波形 这里是写入电源控制寄存器的值0x00 读取到的电源控制寄存器与温度寄存器的值 以下是代码 /* 函数功能:初始化
    发表于 12-28 06:40