Categories
程式開發

你写注释吗?写你就输了


本文最初发布于Level Up Coding官方博客,经原作者授权由InfoQ中文站翻译并分享。

我并不是提倡不写代码注释,只是建议不要过于依赖注释,这样可以使代码更干净、更有表现力,这也能提高开发人员的水平。我自己也在寻求编写更简洁的代码,我尽力不编写糟糕的注释,并在可能时重构代码。

你写注释吗?写你就输了 1

这篇文章的标题可能会让你情绪激动,但请先耐心听我说完。在适当的位置写下适当的注释可能非常有用,但是没有什么比无用的注释更让代码混乱了。在某些情况下,我敢说,注释可以弥补我们在代码中没有完全表达出来的意思。因此,写注释不值得赞美,而是应该停下来问问自己,是否有更好的方式可以用代码来表达自己。

带有少量注释的清晰而富于表现力的代码,要比带有大量注释的混乱而复杂的代码好得多。如果你已经把代码弄得一团糟,不要花时间写注释来解释,而是要花时间梳理代码。如果每次写注释的时候,你都冥思苦想,觉得自己的表达能力不足,那么最终你就会写出简洁明了的代码,完全没有必要写注释。鼓励自己用代码表达。

为什么对注释如此不屑?

因为它们会说谎,还会把代码弄得乱七八糟。虽说并非总是如此,也并非有意如此,但却经常如此。糟糕的代码和带有大量注释的代码之间有很高的相关性。注释存在的时间越久就越容易偏离它们所描述的代码——在某些情况下,它们可能是完全错误的。实际上,随着代码库和团队的增长,维护注释成了不可能的事情。

注释不同于《辛德勒的名单》。它们不是“纯善的”。事实上,注释充其量是一种必要的恶。——Robert C.Martin

当谈论关于注释的话题时,很重要的一点是,我们要看一下什么是恰当的注释,什么是糟糕的注释,这样我们才能学会写更好的注释,或者完全避免注释。

恰当的注释

并不是所有的注释都是不好的——有些注释实际上非常必要。

出于法律目的的注释

有时候,你可能需要出于法律目的编写特定的注释,比如开源项目的创作许可。一些现代化的IDE和文本编辑器会自动将它们折叠起来,保持工作区的整洁。

魔术表达式

如果你有一个复杂的SQL或正则表达式,它以神奇的方式做了一些令人兴奋的事,那么请务必注释,以便让读者更容易理解,因为我们都不是Regex忍者。

// 匹配电子邮件地址的正则表达式
var re = /^(([^()[]\.,;:[email protected]"]+(.[^()[]\.,;:[email protected]"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
// 注意:添加一个富于表现力的函数名,注释就变得没有必要了
function validateEmail(email) { 
    var re = /^(([^()[]\.,;:[email protected]"]+(.[^()[]\.,;:[email protected]"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}

说明意图

在某些情况下,注释有助于解释决策或特定解决方案背后的意图。例如,测试套件中的一条注释告诉我们添加这行代码是为了降低死锁的几率。

for x in range(1, 500):
  
  # 这是运行多个并行测试时预防死锁的最好方法
  time.sleep(0.5)
  runTest(x)

结果预警

用注释说明代码可能会产生严重的或可怕的后果,甚至鼓励这样做。在本例中,开发人员让读者知道,当与回调函数一起使用时,QT函数不是线程安全的。一般来说,如果一条注释可以避免某个人在编程时陷入绝望,那么它就是有用的。

"""
  许多Qt函数都不是线程安全的。如果你使用回调函数,
  即使你在所有绘制调用代码的周围都加上锁,你也会遇到段错误,
  因为Qt的主事件循环仍在运行,并且使用了没加锁的资源。
"""
from multiprocessing.pool import ThreadPool
import sys
from threading import Lock
import time
from PyQt5 import QtCore, QtWidgets
class Task(QtCore.QObject):
    updated = QtCore.pyqtSignal(int, int)
    ...............
    ...............

TODO注释

这些注释可以帮助我们标记那些我们认为应该做,但是由于某些原因没有做到的事情。它可能会提醒你删除废弃的特性,或者请求其他人查看某个问题。它可能是要求其他人想一个更好的名字,或者是提醒他们根据计划事件做出修改。

请记住,TODO注释不是在系统中留下糟糕代码的借口。本质上,每一行代码都是一种负担——最安全、最快的代码是根本没有代码。

现在,大多数优秀的IDE都提供了特殊的指令和特性来定位所有的TODO注释,所以不太可能漏掉它们。尽管如此,你也不希望代码中到处都是TODO。所以要经常浏览一下,删除那些你能删除的。

糟糕的注释

这个清单比较长,但在本节中,我们将看到一些更为老生常谈而又随处可见的注释。

明知故问的注释

有些注释的意思显而易见,即它们没有增加任何实际的价值,而且大多是噪音。

下面是一个开源项目的代码片段,其中包含大量明知故问型的注释,这些注释使代码变得混乱而晦涩。它们所提供的信息并不比代码本身多,而且在某些情况下,阅读注释的时间甚至比阅读代码长。

/**
* 与该容器相关的集群
*/
protected Cluster cluster = null;
/**
* 人类可读的容器名 
*/
protected String name = null;
/**
* 该容器的父容器
*/
protected Container parent = null;
/**
* 创建一个Loader配置父类加载器
*/
protected ClassLoader parentClassLoader = null;

不清不楚的注释

如果你写注释是为了符合公司规定,或者你只是觉得有必要添加一些注释,那么你在注释时就不会进行适当的思考。所以,如果你真的写了一条注释,花点时间让它对阅读它的人有所帮助。

def load_config():
  try:
    do_useful_stuff()
  except Exception as ex: 
    # 如有异常,退回到默认状态。

在这个例子中,作者想要传达一些有关异常情况的重要信息。但这条注释没能解释清楚我们将退回到什么样的默认状态。如果一条注释要求我们转到另一个模块来找出默认值,那么它就没有发挥应有的作用。

注释掉代码

在团队准备好删除代码之前先将其注释掉似乎是一个好主意,但是不要这样做。注释代码是一种弊端,团队中的其他成员不会删除它,因为他们会认为它很重要。我们不是都在使用源码控制吗?所以我们不需要保留旧的代码。我们可以跳到任何我们想要的版本。

噪音注释

有些注释毫无意义,纯粹是噪音。时间久了,我们的大脑就会走马观花,我们也会开始跳过那些需要注意的重要注释。考虑一下下面的例子,其中的注释提供了很多价值吗?

-----------------------------
# Exhibit A
# 默认构造函数
def get_todays_date():
  return date.today()
-----------------------------
# Exhibit B
# 返回月份的天
# @return: 月份的天
def get_day_of_month()
  return day_of_month

用编写干净代码的决心取代制造噪音的诱惑,你将成为一个更好、更快乐的程序员。

强制性注释

这肯定会引起争议。如果规定每个函数都需要一个Java文档或Python docstring,是不是有点傻?大多数时候,类或函数名已经告诉我们注释所描述的内容,它们是多余的。在这个例子中,注释的数量比代码的数量还多——这让我很恼火。

class ComplexNumber: 
    """ 
    这是一个用于复数的数学运算类。
      
    属性:
        real (int):复数的实部。
        imag (int):复数的虚部。
    """
  
    def __init__(self, real, imag): 
        """ 
        ComplexNumber类的构造函数。
  
        参数:
           real (int):复数的实部。
           imag (int):复数的虚部。  
        """
  
    def add(self, num): 
        """ 
        该函数用于复数求和。
  
        参数:
            num (ComplexNumber):要加的复数。
          
        返回值:
            ComplexNumber:包含和的复数。
        """
  
        re = self.real + num.real 
        im = self.imag + num.imag 
  
        return ComplexNumber(re, im) 
  
help(ComplexNumber)  #访问类的docstring 
help(ComplexNumber.add)  # 访问方法的docstring 

使用好的函数名或变量名

你可以使用更具表达性的函数和变量名替换注释,从而使代码更简洁。考虑下面的例子,第一个例子中的注释就变得没有必要了,因为有一个更好的函数名可以准确地告诉读者这个函数做了什么。

# 检查日期是否是过去的日期
def check_date(date):
   if date < today:
     return true 
   return false 

def is_past_date(date):
   if date < today:
     return true 
   return false

注释不能弥补代码的糟糕

编写注释有一个比较常见的原因是糟糕的代码。我们以前都见过这种情况,在某种程度上,我们自己也犯过这样的错误。我们写一个模块或类,我们心里知道它混乱而无序。我们知道它一团糟。所以我们对自己说,“哦,我最好加下注释!”不!你最好把代码梳理清楚!

/* 
这段代码糟透了。我知道,你知道,每个人都知道。
我们假装什么都没发生,然后继续前进。以后你叫我白痴好了。
*/

小结

我并不是提倡不写代码注释,只是建议不要过于依赖注释,这样可以使代码更干净、更有表现力,这也能提高开发人员的水平。我自己也在寻求编写更简洁的代码,我尽力不编写糟糕的注释,并在可能时重构代码——将我的代码从宜家的一幅画变成梵高的作品。

所以让我们约法三章,不要写这么多注释。

原文链接:
Every time you comment code — you’ve already failed.