没有混淆 Perl 代码竞赛,因为毫无意义。
—Jeff Polk
无论你使用何种语言编写代码,作为程序员,你的任务都是尽你所能地利用手头的工具。优秀的程序员可以克服糟糕的语言或笨拙的操作系统,但即使是出色的编程环境也无法拯救糟糕的程序员。 —Kernighan 和 Pike
自编程诞生以来,程序员们一直在争论不同编程语言的优缺点。每个程序员都有自己喜欢的通用编程语言,许多人也有不喜欢的语言。如果程序员足够老,通常不喜欢的语言就是 Fortran。世界已经见识了太多糟糕的 Fortran 代码,以至于该语言的名称现在已成为糟糕编码的代名词。我们中的许多人从未见过真正的 Fortran 代码,但我们知道程序员所说的“你可以用任何语言编写 Fortran 代码”是什么意思。
我职业生涯的很大一部分时间都与 Fortran 息息相关。信不信由你,你可以编写好的 Fortran 代码,也可以编写糟糕的 Fortran 代码。今天没有人愿意使用 Fortran 编程,因为有很多更好的替代方案。但是,尽管 Fortran 有很多障碍,你仍然可以使用它编写可用且可维护的程序。
优秀的编码具有超越所有通用编程语言的特性。如果你全身心投入,几乎可以在任何代码中实现良好的设计和透明的风格。仅仅因为一种编程语言允许你编写糟糕的代码,并不意味着你必须这样做。而且,即使一种编程语言被设计为提倡良好的风格和设计,如果程序员足够有创造力,仍然可以用它来编写糟糕的代码。你可能会在只有一英寸水的浴缸里淹死,你也可能很容易地用一种没有 goto 或行号,具有异常处理、泛型类型和垃圾回收的语言编写一个完全不可读且无法维护的程序。无论你编写的是 Fortran 还是 Java,C++ 还是 Smalltalk,你都可以(并且应该)选择编写好的代码而不是糟糕的代码。
人类并非仅仅生活在客观世界中,也并非仅仅生活在通常理解的社会活动世界中,而很大程度上受制于已成为其社会表达媒介的特定语言。 —Edward Sapir
每种模式都描述了在我们环境中反复出现的问题,然后描述了该问题解决方案的核心,这样你就可以使用此解决方案数百万次,而永远不会以相同的方式执行两次。 —Christopher Alexander
我将首先声明我的偏见,然后尝试通过示例来证明它们是合理的。
关于语言决定论有一个著名的观点,称为萨丕尔-沃尔夫假说,由爱德华·萨丕尔和他的学生本杰明·李·沃尔夫提出。大致来说,它认为我们语言的词汇和语法指导并限制了我们看待世界的方式:形式决定内容。Edsger Dijkstra 认为,使用 Fortran 或 Basic 编程不仅注定我们会产生糟糕的代码,而且还会永远腐蚀我们。
编程语言决定论的思想有一定的道理,但被高估了。因为我们经常在 C、Perl、Scheme、Smalltalk 等语言中解决相同的问题,所以我们通常可以找到一种方法来分析它们并使用通用设计来编写解决方案。有时,特定语言的特性会使特定的解决方案更加优雅和易于理解,在这种情况下,形式会影响内容。但是这些语言具有足够的共同点,可以共享许多设计。C 可能有前缀递增和后缀递增运算符,但你仍然可以在任何支持变量的语言中将 1 添加到变量。
C 不包含对面向对象编程等特性的内置支持。但是,如果设计足够简单并且可以解决你的问题,你仍然可以在 C 中使用面向对象的设计。许多用 C 编写的数据结构和库都具有对象的形式和功能,即使该语言不直接支持继承、包、私有成员和其他特性。vnode 结构就是一个很好的例子。用于文件系统操作的 vnode 抽象层的想法在类 Unix 操作系统中变得非常流行,因为它显然是一个很好的设计,并且因为 C 没有阻碍它,尽管它也没有提供任何帮助。我在其他想法和其他语言方面的经验也类似——如果设计良好,并且语言没有阻碍,那么程序员就会采用并适应它。许多好的编程思想可以在不同的语言和操作系统环境中重复使用数百万次。
你不应该理解这个。 — Version 6 Unix 中 swtch() 中非本地 goto 上方的注释
我们可以对不同通用编程语言中的优秀代码进行一些概括,因为我们编写代码的原因是相同的,包括以下两个主要原因。
首先,我们编写代码是为了设计问题的解决方案。因此,好的代码具有良好的设计元素。在现实世界中,许多与你的问题类似的问题已经被解决,因此通常有很多好的设计元素可以用来组合成你的解决方案。解决方案的成功取决于设计的质量;糟糕的设计通常会导致错误的“解决方案”(也称为错误)。该设计可能无法很好地扩展,或者可能难以扩展,或者可能非常复杂,以至于查找和修复错误成为一项极其痛苦和耗时的任务。好的设计很容易识别,因为它看起来很直观并且(通常)很熟悉。
其次,我们编写代码是为了向人们传达我们的设计。机器只对 1 和 0 感兴趣。它们根本不在乎高级编程语言。另一方面,当人们维护直接以 1 和 0 编写的代码时,效果非常差。对于人们来说,形式和风格对于传达内容非常重要。好的形式是透明的:读者“看到”的是内容,而不是形式或风格。好的形式也很优雅。它不会使页面杂乱无章,也不会用过多的信息使你的头脑混乱。当你的代码既易于理解又包含少量干扰时,你的读者将最彻底地吸收你的代码。
如果你怀疑你编写代码是为了人而不是为了机器,我建议你看看机器生成的代码:(见图 1)
如果你编写和维护这样的代码,你很快就会疯掉。所有这些数字是什么意思?机器知道,但我们不知道。形式和风格对于好的代码至关重要,比许多程序员认为的要重要得多。
有能力的程序员充分意识到自己头骨的严格有限的大小;因此,他以充分的谦逊态度对待编程任务,并且尤其要像躲避瘟疫一样避免使用巧妙的技巧。 —Edsger Dijkstra
我的理想是,我应该能够通读好的代码,并发现设计是如此明显和直观,风格是如此自然和透明,以至于我可以用我的母语或多或少地实时准确地描述它。我已经花了将近 30 年的时间维护代码,其中大部分代码是别人编写的,到现在我确切地知道我想看到什么,并且我尝试将这些知识应用到我自己的代码中。
优秀设计的要素已经在其他地方得到了很好的描述,所以我在这里将重点强调形式和风格,特别是那些适用于各种编程语言的特性。我在优秀代码中寻找的许多特性都可以在每种通用编程语言中找到,甚至包括 Fortran。以下是其中一些。
分组
每种现代通用编程语言都为空格提供了很大的自由度。通常,空格仅在标识符之间是必需的,而在标点符号和运算符周围是可选的。空行、缩进和表达式内的空格都是允许的,但都是可选的。尽管如此,空格在好的代码中是必需的,因为它实现了分组。
当我们编写自然语言时,我们使用空格作为一种标点符号,以标记段落分隔符和标题。空格在编码中具有相同的功能。当然,编译器并不关心
这
=
是
+
非常
*
令人恼火的
;
你应该在代码中使用空行和缩进,以强调其逻辑结构,作为读者的指南。如果代码使用空行进行了很好的分段,则更容易导航。空格不足会使结构难以看清,几乎同样重要的是,会使屏幕或页面上的代码难以跟踪。如果你短暂地将目光从一大块代码上移开,例如,检查变量声明,当你回头看时,可能很难找到你的位置。如果编程语言允许你省略运算符和标点符号周围的空格,你可以在一行代码中获得相同的效果
This=is+almost*(as-annoying);
你可以使用行内空格来强调逻辑部分,例如赋值的左侧和右侧
This = is + much * (more - readable);
滥用空格会造成视觉混乱,具有讽刺意味的是,这会导致与缺少空格相同的问题:文本变得难以导航,因为结构和地标消失了。但是,我见过的大多数糟糕代码似乎都倾向于空行太少。随着时间的推移,我发现我自己的代码中有更多的空行用于分组。我尽量不让它太零碎。这有点像报纸中的段落与小说中的段落之间的区别——形式在报纸中需要是透明的,而小说通常是形式和内容之间的深刻关系。
让我们深入一点。机器不在乎一个组有多长。那么我们为什么要关心呢?人们的短期记忆容量有限。它可能容纳七个项目。(据说这就是为什么美国的本地电话号码有七位数。)你可以通过分块来绕过这个限制。如果你可以将几个项目组合在一起,则该组本身就成为一个项目。人们可以在他们的头脑中构建组的层次结构,但在每个级别,如果项目数量有限,他们会更好地记住事物。如果你的代码超过了限制,人们就会感到困惑并开始笨手笨脚。
这种心理限制在代码的每个级别都会受到影响,一直到语句。如果一行代码中挤满了操作,则会变得难以阅读和理解。我见过很多代码使用非常宽的行或更短的缩进停止来在每行中打包更多信息。我认为这是错误的。每行代码更多不是优点;可读性才是优点。
从几个元素蔓延到许多元素的语句需要分解为多个语句。大多数现代语言都允许在语句中间换行,但是换行并不能使语句更易于阅读。有时,接口会决定语句中元素的数量,而你几乎无能为力。即使那样,你也可以最大程度地减轻痛苦
Can(you, tell, at + a, glance, which * of, these, parameters, is(the, eighth), one ? yeah : sure);
当你有这么多参数时,可能是时候切换到传递记录而不是单个参数的接口了。
熟悉度和模式降低了组中项目的内存负担。视觉模式可以使代码非常易于阅读
It = 0;
is = 0;
pretty = 0;
obvious = 0;
what = 0;
this = 0;
code = 0;
does = 0;
even = 0;
at_this = 0;
length = 0;
你可以通过保持组尽可能自然来利用这种记忆技巧。你可以在以下项周围放置空行
• 类或记录声明中的相关字段声明
• 相关常量
• 相关局部和全局声明
• 代码中的相关变量和字段初始化
• 相关算术语句
• 相关输出语句
• 相关防御性编程元素
这是一个简单的原则:相关的事物作为一个组更容易记住。
缩进也提供了分组。这是一种与空行不同的视觉指示,但与之相关。我确信我们所有人都见过一些错误,其中缩进给出了非常误导的结构感,即使缩进在编程语言中没有任何语法意义。良好的缩进实践在 Lisp 等语言中尤为重要,在这些语言中,结构的视觉提示更难发现。幸运的是,各种语言的标准编码风格都非常强调缩进,因此现在滥用缩进的例子较少。
与缩进相关的是制表符。我指的是将一行中的元素与垂直上方和下方的元素对齐的做法。制表符似乎是一种常见的做法,主要用于强调重要的运算符(例如赋值)。但是,宽制表符往往会使我在视觉上将元素与垂直维度而不是水平维度相关联,即使水平分组更自然
I = because;
scan = these + items;
down = are(associated);
the = vertically;
column = “so it feels”;
rather = natural;
than = to(read, them);
across = that % way;
这种代码可能非常难以正确阅读。当平均水平间距较小时,制表符效果最佳。由于这个问题,我通常避免在代码中使用制表符,尽管有时编码标准会强制使用制表符。
注释的制表符问题较少。当一列是代码,另一列是注释时,它们确实具有自然的分组。这是一个视觉线索,表明注释不是代码。当制表符注释不太靠近时,它们会突出显示并可以用作视觉地标。即使使用注释,宽制表符仍然倾向于让我向下阅读列而不是横向阅读。它还会产生更多的视觉混乱,从而隐藏程序的结构。
自然分组对于可读性非常重要,以至于可能值得更改某些代码的结构以对相关元素进行分组。有时,深度嵌入的控制结构会拆分组,甚至导致内存限制问题。如果可以通过使用替代语法或将代码移动到子例程来展平控制结构,则代码可以变得更易于阅读。
这个问题在自然语言中很明显。在语言学中,有一种著名的现象称为自嵌入,它证明了这个问题
伊拉克指责为间谍的联合国检查员未能找到的大规模杀伤性武器是
布什思想的虚构。
这是语法正确的英语,但完全不可读。“大规模杀伤性武器”是“虚构的”,但这些短语在视觉上相距甚远,并且很难匹配。在代码中,我们有时可以使用缩进来使分组起作用
if (WMDs != FIGMENT)
if (WMDs == 0)
if (spies(&inspectors) == TRUE)
dump(&Tenet);
else
withdraw_from(UN);
else
invade(&Iraq);
else
seek(PSYCHIATRIC_HELP);
但这仍然会通过将 else 子句放在远离 if 测试的位置来破坏自然分组。请注意,如果我们展平此代码,可读性会发生什么变化
if (WMDs == FIGMENT)
seek(PSYCHIATRIC_HELP);
else if (WMDs != 0)
invade(&Iraq);
else if (spies(&inspectors) == FALSE)
withdraw_from(UN);
else
dump(&Tenet);
当然,你也可以通过将深度嵌套的代码推送到子例程来避免过度嵌入。
注释
在现代通用编程语言中,注释在语法上是空格。编译器会忽略它们,但读者不会!
每个人都有自己最喜欢的糟糕注释。
/* add one to I */
i = i + 2;
因为没有机械检查来验证注释的正确性或适当性,所以注释很容易被滥用。重复显而易见的注释只是视觉混乱。错误描述代码的注释对读者来说是一场灾难。当有人对代码进行修复而不是对注释进行修复时,第一种注释通常会变成第二种注释。
好的注释不应重复代码;好的代码应该自己说明问题。相反,好的注释应该激励或解释代码,而无需引入属于代码本身的细节。一个糟糕的注释
/* shift x by 2 and add to base */
result = base + (x << SCALE_FACTOR);
一个更好的注释
/* 内存已分区。缩放索引。*/
result = base + (x << SCALE_FACTOR);
我坚信单行和多行注释应包含真实的句子,而不是电报文或伪代码。这似乎需要很多工作,但我也相信,一旦你编写了好的代码并避免了糟糕的注释,你就会发现,更少的好的注释将比大量的糟糕注释更有价值。人们会欣赏注释中的真实句子,因为他们不必再与另一种语言作斗争。阅读代码已经够难了;为什么要让读者做更多的工作来理解注释?
好的代码应使用“最少惊讶原则”。如果读者没有被警告要预期棘手的地方,他们通常会错过。一个好的块注释是标记需要仔细检查的代码的好方法。好的代码应尽可能避免依赖技巧,但是当技巧不可避免时,请竖起那些橙色锥体和闪烁的灯光
/*
* 如果新进程因为被
* 换出而暂停,则将堆栈级别设置为上次调用
* savu(u_ssav)。这意味着返回
* 紧跟在 aretu 调用之后执行
* 实际上是从执行了
* savu 的最后一个例程返回的。
*
* 你不应该理解这个。
*/
if (rp->p_flag&SSWAP) {
rp->p_flag =& ~SSWAP;
aretu(u.u_ssav);
}
(不,’=&’ 运算符不是错别字。)
注释可以是结构的重要视觉提示。在 C 注释中排列星号,在 Lisp 注释中排列分号,或在 shell 注释中排列井号 (#) 会吸引眼球。像这样的视觉主题也是使注释看起来与代码不同的好方法。
名称
名称中有什么?很多。好的名称对于好的代码至关重要。你可以选择你在代码中使用的大多数名称。像注释一样,名称对机器没有任何意义。它们除了作为代表程序元素的字符字符串(例如变量、函数、类或更奇特的东西)之外,没有任何意义。像注释一样,名称对人们来说意义更大。为了使代码可读,你需要明智地选择名称。
名称中什么重要?正如我们之前看到的,熟悉度降低了脑力劳动量,因此在熟悉的上下文中,熟悉的名称更容易理解。自古以来,程序员就使用 i 作为索引变量的名称。这很无聊,但是你在自己的代码中使用 i 作为索引变量不会出错。
名称越熟悉,它向读者传达的信息就越多
Int
main(int argv, char **argc)
{
[...]
}
如果你懂 C 语言,前面的代码会让你感到窒息。很久以前,我实际上接触过一些故意这样编写的 C 代码。在 C 语言中,名称 argc 和 argv 不是任意的。如果你在函数中看到它们,即使是 main() 以外的函数,你也会确切地知道它们应该是什么意思。这些名称不是 ANSI C 标准强制要求的,但它们仍然是标准做法。无视它们的标准用法将给你带来(巨大的)危险。
即使对于不属于标准实践的名称,我们也需要考虑这个问题。如果你使用名称 pShl 作为指向 SHL_NODE 结构的局部变量,则应保持一致,并且永远不要在程序中的其他任何地方将该名称用于其他目的。更好的是,你应该在整个代码中在相同的上下文中为相同的目的使用相同的名称。如果你保持一致,那么当人们在你的代码中的任何位置看到 pShl 时,即使没有看到它的声明,他们也会确切地知道它的作用。通过使用熟悉的名称来减轻读者记忆的负担将使代码更具可读性。
我再怎么强调也不为过。明智地选择名称可能是编写可读代码的最大因素。熟悉且明显的名称使代码更具可读性。尽可能使用熟悉且明显的名称,并对你使用的名称保持一致。
此外,与注释一样,在命名中,描述性和视觉混乱之间存在张力。在这些极端之间存在一种平衡,这是我在阅读和编写大量代码后设法达到的。我尝试对常用元素使用简短、有力的名称,而对较少见的元素使用更长、更具描述性的名称
char c;
off_t o2;
extern iso_t neodymium_148;
extern iso_t *actinide_series[14];
非常长的名称会非常碍事,以至于它们会模糊代码的结构。另一方面,将非常长的名称压缩成首字母缩略词可能会产生看起来像线路噪声的代码。
尽管名称是词法原子,但它们是可组合的
mol_t calcium_carbonate;
mol_t calcium_magnesium_carbonate;
mol_t potassium_magnesium_iron_aluminum_silicate_
hydroxide_fluoride;
这是创建熟悉度的另一种好方法。我们希望对相似的元素使用相似的名称。在自然语言中,我们用称为语素的元素来构建单词,例如 carbon + ate。我们在代码中对名称执行相同的操作:off_t 是 off + _t。组合有时会创建笨拙的名称
void XrmStringToBindingQuarkList(const char *,
XrmBindingList, XrmQuarkList);
既然我们正在谈论极其长的名称,我不妨提及我的偏见
I_find_underscores_easier_to_read_than(lotsOfStudlyCaps);
下划线有点像标识符内的空格。但是,理性的人们可能在这个重要问题上存在分歧。
一致性
风格指南:我讨厌它们。毕竟,我知道哪种风格是最好的:我的风格!风格指南通常看起来是枯燥的、看似武断的规则列表,这些规则限制了我的创造力。阅读它们让我昏昏欲睡。
但是,当我维护代码时,我会抛开个人风格,并尝试与项目的风格相匹配。我希望我的代码看起来与其他所有人的代码完全一样,至少在风格指南方面是这样。这样做的原因,再次强调,是熟悉度。(这听起来是不是很熟悉?)如果在整个软件项目中使用相同的编码约定,维护人员将习惯这种风格,并且它将神奇地对他们变得透明。他们将看到代码,而不是风格。
缺乏一致性是糟糕代码的标志之一。如果有 30 个不同的人在一个源文件上工作,当我阅读它时,我真的、真的不想看到 30 种不同的编码风格或命名方案。试图在这样的代码中找到结构会成为一场噩梦。程序员必须谦虚地接受,为了使代码可读,他们最喜欢的风格不如已建立的风格好。
总结一下,无论使用哪种编程语言,好的代码都应该
• 避免混乱
• 使用分块
• 使用熟悉度
• 防止惊讶
• 保持一致
向以前接触过 Basic 的学生教授良好的编程风格实际上是不可能的;作为潜在的程序员,他们的思想被摧残,以至于失去了再生的希望。 —Edsger Dijkstra
我不是 Dijkstra 或 Kernighan,但我从小就开始编码。我的思想有可能被我在高中时编写的所有 Basic 代码摧毁了。我仍然记得发现 GOSUB 并找到在我的(少数)程序中使用它的方法。我也编写了我那份 Fortran 代码。我还记得当我第一次接触 Algol 时,试图将我的一些 Fortran 实践强加于 Algol。我最终使用多种编程语言完成了编码项目。过了一段时间,我培养了识别它们之间共同点的眼光。
就像色情作品一样,我一眼就能看出糟糕的代码。通常,我也能一眼看出好的代码。我认为大多数其他程序员也是如此。从阅读大量糟糕的代码(和一些好的代码)中,我意识到代码的编程语言对于代码质量的重要性不如代码被(滥)用的方式。我认为我已经找到了一些合理的解释,说明为什么有些代码看起来不错,而有些代码看起来很糟糕。
我仍然看到很多糟糕的代码。有很多借口
• 代码是在紧张的期限内编写的。
• 这是某人的第一个大型编码项目。
• 它只应该是一个原型。
• 它最初是一个个人项目。
编写好的代码而不是糟糕的代码所需的工作量实际上非常小。随着时间的推移,当不同的人维护软件时,好的代码的回报实际上非常大;从一开始就编写好的代码以外的任何代码都是没有意义的。
喜欢它,讨厌它?请告诉我们
[email protected] 或 www.acmqueue.com/forums
Donn M. Seeley 是 Wind River Systems 的高级技术人员。他是 Berkeley Software Design, Inc. 的联合创始人,该公司是 4BSD 的第一家商业供应商。他是《蠕虫之旅》的作者,该书是关于 1988 年 Morris Internet 蠕虫事件的原始论文之一。他目前在 Wind River Systems 从事嵌入式系统技术工作。
© 2004 1542-7730/04/1200 $5.00
最初发表于《Queue》第 2 卷,第 9 期—
在 数字图书馆 中评论本文
Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息方面需要权衡:这可能会使编译速度变慢。链接时优化等技术可以为你提供两全其美的效果。编译器中的优化在不断改进,即将到来的间接调用和虚函数分派方面的改进可能很快会带来更快的多态性。
Ulan Degenbaev, Michael Lippautz, Hannes Payer - 作为合资企业的垃圾回收
跨组件跟踪是解决跨组件边界的引用循环问题的一种方法。只要组件可以在 API 边界上形成具有重要所有权的任意对象图,就会出现此问题。CCT 的增量版本在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。
David Chisnall - C 不是一种低级语言
在最近的 Meltdown 和 Spectre 漏洞爆发后,值得花一些时间来研究根本原因。 这两个漏洞都涉及到处理器推测性地执行指令,绕过某种访问检查,并允许攻击者通过侧信道观察结果。 添加这些导致这些漏洞以及其他一些漏洞的功能,是为了让 C 程序员继续相信他们是在用低级语言编程,尽管这种情况已经几十年没有发生了。
Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应依赖我
大多数网站使用 JavaScript 库,其中许多库已知存在漏洞。 了解问题的范围,以及库被包含进来的许多意想不到的方式,仅仅是改善现状的第一步。 本文包含的信息旨在帮助改进社区的工具、开发实践和教育工作。