META:本文附有一个 GitHub Repo ,其中包含一个示例 Xcode 项目。我们将链接到该 repo 中的标签,以表示我们讨论中的各个步骤。我们还将有一个包含生成的文档的目录。它也将有多次修订。有一个明确的思想流派,它说“如果我记录我的代码,我就会失业。”我不知道其他任何人,但我无意度过余生,为老爷车工作。我想学习新东西,让自己陷入困境,探索新的视野。如果我故意让别人为难;特别是那些经验比我少的人,为了接管和理解我的代码,当世界转向燃油喷射或电动汽车时,我可以期待疏通化油器。此外,我对自己的工作感到有些自豪。我认为我做得相当不错,我希望人们看到我在做什么,并了解我是如何做的。我并不是从软件开发人员开始的。我对计算机的第一次体验首先是作为一名电子技术员,然后是一名电子工程师。
正因为如此,我很不安全,从这个新奇的“软件”开始。我花了很多时间,环顾四周,看看其他人是如何做到的。我非常清楚地记得一件事,就是对清晰记录的代码施加的压力。在那个年代,代码非常原始。我们有像 BASIC、FORTRAN、COBOL 和 Assembly 这样的语言。 C 刚刚开始显示出它最终取得的巨大成功的迹象。我们无法真正记录机器代码;除了在开发阶段,因为没有实际的源代码。这是我们输入到处理器的执行堆中的一串数字。我们可以记录纸本,计算代码的地方(那时,我们会在纸本上计算机器代码;而不是文本编辑器)。由于汇编程序实际上只是一系列简单的线性步骤(例如,我们不能只是说“将变量 B 的值设置为变量 A 的值。”我们需要说“移动由将变量 A 的地址放入寄存器 A,然后将寄存器 A 中的值移动到变量 B 的指针指向的内存位置”),这意味着我们可以在 ASM 代码的每一行附加一个简单的单句注释,例如所以:0045 3A943E LDA Byte5;得到通讯。 map0048 3E01 ANI Hpibon 的灯字节;看灯是否已经亮了004A C25700 JNZ Go_0 ;如果是 on004D 21943E LXI H,Byte5 则跳过其余部分;获取位图字节的地址 60050 3E01 MVI A,Hpibon ;拿到面罩打开HPIB灯0052 B6 ORA M; OR 它与 byte0053 77 MOV M,A 的内容;重新存储结果 • • •020A 21943E Fploff LXI H,Byte5 ;获取位图字节的地址 6020D 3E03 MVI A,Serlon+Hpibon ;获取开/关灯的掩码020F 2F CMA ;补充 it0210 A6 ANA M ;并将其带回已经存在的内容0211 77 MOV M,A ;把它放回原处 那代码直接来自我的第一个工程项目。它是 1987 年的 Intel 8085 代码。
如果 IEE-488 接口上有任何交互,所有这些代码都可以打开,然后再次关闭指示灯。当数据流入和流出设备时,这就是我们如何得到一点点闪烁的光。当数据处理开始时,我们会打开灯(第一个代码块),在处理完数据后,我们会调用一个例程来强制关闭灯(第二个代码块)。我的 BASIC 和早期的 C 代码几乎一样“细化”,所以我早期的程序往往有迂腐的、每行都有一个注释的文档。我的第一个软件项目之一是担任 100,000 多行 1970 年代老式 FORTRAN IV 代码的维护程序员,这些代码自编写以来几乎每个新程序员都使用过,并且文档为零。几乎是“意大利面条式代码”的柏拉图式理想。这是一个一直伴随我直到今天的教训。我从来不想让其他人受制于那个。现在,有很多因素会影响我编写文档的方式,这就是我在这篇文章中真正要介绍的内容。我只想做一个简短的“我不得不步行上学,上坡,在雪地,在七月”侧边栏,以建立一些背景。在我们讨论程序问题之前,我想谈谈我在为我的工作创建项目文档时使用的工具。如今,大多数人熟悉的文档类型是 Markdown README 文件。
Markdown README 最著名的实现是 GitHub 实现,尽管现在几乎每个存储库系统都这样做。它的工作方式是,如果我们在源代码目录中有一个文件,称为“README”或“README.md”,站点引擎会将其呈现为 Markdown 。这使我们能够有一个相当好的格式显示,同时也有非常易于理解的 Markdown 文件的“源代码”。作为一个基本的经验法则,我所做的每个项目的根目录中都有一个 README.md 文件。如果有我认为需要额外文档的子目录,我也会将 README.md 放入其中。任何包含 README.md 文件的目录都将由 GitHub(或其他 repo 系统)呈现。当我们转到 GitHub 页面并向下滚动时,我们将看到 README.md 文件,呈现为:CocoaPod 页面(注意:CocoaPod 已被弃用,因此无法保证它会持续多久)。请注意 README.md 在两个存储库中都呈现。另请注意,CocoaPods 将 CHANGELOG.md 文件呈现到单独的页面中。这很有用。 README 可以非常健壮;包括图像和代码示例。如果我包含图像,它将在同一目录中引用相对于自述文件的图像。但是,当我开始变得更喜欢(稍后会详细介绍)时,我可能需要为图像执行绝对 URI。
这似乎是“不费吹灰之力”,但对于版本控制系统,我们还有另一个地方可以添加文档:提交新代码更改时提供的注释。我看到人们添加了诸如“asdf”或“qwerty”之类的有用评论,但这是我们可以添加一些高度相关且及时的文档的地方。稍后我会更深入地探讨这个问题。 Doxygen 是一个成熟的、维护良好的“autodoc”生成器。它由荷兰程序员维护,非常强大。我将在我的编码中详细说明我如何支持它。 HeaderDoc 是 Apple 对 JavaDoc 的实现。注意“是”。它已被淘汰,取而代之的是基于 Markdown 的文档系统。稍后我会讲到。它太酷了。我比 HeaderDoc 更喜欢它。 Jazzy 是一个非常酷的 Ruby 实用程序,由realm.io 上的人们编写。我将它用于我所有的 Apple 文档。它与 Apple Inline Markdown 结合使用。正如我所说,在早期,我会记录每一行代码,但这不仅是矫枉过正;它有文档过时的风险,这更糟。
在接下来的讨论中,我将使用 Swift Programming Language 中的示例,因为这是我使用最多的语言。它也是一种现代、强大的语言,具有许多我可以练习的特性和功能。我开始意识到最重要的内联文档与我们为什么做某事有关;不是我们在做什么。例如,没有人想读“// Set the value of b to 3”,这是一行看起来像 let b = 3 的代码。那太愚蠢了。但是,他们可能想知道“ // 将 b 的值设置为我们将要进行的迭代次数。” let b = 3 // 将 b 的值设置为我们将要进行的迭代次数 拥有一致的命名约定很重要。大多数拥有样式指南的公司都在该指南中包含命名约定。他们的名字有“哲学基础”。我尝试为我的项目做同样的事情。
let b = 3 // 将 b 的值设置为我们将要进行的迭代次数 “b”没有任何意义;特别是因为我们正在谈论迭代最大索引。这将更好地命名为“iterationCount”或“maxIterations”:如今,我想减少评论的数量,我可以通过结合清晰的命名约定和直接的编程来做到这一点。让我们看一个代码块的例子,它遍历整数集合,并向它们添加一个值。这是一种相当基本(且常见)的操作类型: let ofst = 5let incArInt = [1, 2, 3, 4, 5].map { $0 + ofst }let valAr = incArInt 现在这就是所谓的“地道的 Swift”,或以“原生”方式表达的 Swift。使用数字文字将偏移值 5 设置为 Int 常量。
正在映射 Int 的数组文字(使用 map() 高阶函数),并返回一个新的 Int 数组。这个新 Array 将是 Array 文字值,偏移量添加到它的每个成员。如果我们想成为真正的速记,我们也可以将偏移量设为文字:当然,这违背了本次讨论的全部目的。让我们假设我们将来会使用偏移量。我将 Array 文字留在那里以帮助我说明问题。现在,我们需要记住的一件事是,谁来清理我们的烂摊子?如果我们知道在我们之后的人将是一位经验丰富的 Swift 程序员,那么使用惯用的 Swift 是不费吹灰之力的。它更有效率,他们会很快理解它。也就是说,我的经验是维护程序员往往是一些初级员工。他们很可能是两个月前刚刚学习 Swift 基础知识的 JavaScript 程序员。
在这种情况下,惯用的 Swift 可能适合教他们,但如果我们希望他们找到并修复错误,或向现有代码库添加功能,则可能不太好。解决这个问题的一种方法是将惯用的 Swift 重写为他们可能会觉得更熟悉的形式: let integerOffset: Int = 5let currentValuesIntegerArray: [Int] = [1, 2, 3, 4, 5]var workingArray: [Int] = []for value in currentValuesIntegerArray { let tempValue: Int = value + integerOffset workingArray.append(tempValue)}let newIntegerArray: [Int] = workingArray 也就是说,从字面上看,自文档代码,但它不是很好,如果我们的目标是帮助培训维护程序员成为更高级的 Swift 程序员。它基本上是用 Swift 编写的简单、公分母最低的 JavaScript。因此,“中间地带”可能是使命名不那么不透明,并添加一些注释以帮助程序员理解和学习: // The current index table of lines.var textLineIndexes = [1, 2, 3, 4 , 5] // 我们将插入到 text.let 中的行数 insertLineCount = 5let addOffsetFunction = {(currentValue) in return currentValue + insertLineCount} // 我们使用 Array.map() 函数来添加偏移量,如它是最有效的 Swift 过程。textLineIndexes = textLineIndexes.map(addOffsetFunction) 在上面的例子中,我通过指出 Array 是文件中文本行的 Array,使它更“真实”一点,我们将正在向文件中添加一些文本行(我将对此进行扩展)。
请注意,注释相当简洁。我可以说“这是文本文件中的索引表。”,但我说的是“当前的行索引表。”还要注意我提到了为什么我们使用 map() 函数来添加偏移量。这是对培训的一种认可。经验丰富的 Swift 程序员不需要被告知这一点,但是一个新手,可能不了解 Swift 的高阶函数实现,可能需要它。如果我们想成为一个更“惯用”一点,我们可以写最后一行,像这样:当我们将代码提交到版本控制存储库时,我们可以在代码提交中添加一些非常相关的文档。我尝试做的一件事是经常提交,所以每次提交都没有大量的变化,评论可以非常集中,比如“修复错误 #1234。木星环不对齐。我用活动扳手敲了几次,然后他们又回到了同步状态。”在下面的例子中,我将重复整个“留下遗产”比喻,并使用著名的雪莱诗歌“奥兹曼迪亚斯”作为数据集。我们将建立一个简单的函数,它使用换行符将一个字符串数组附加到另一个字符串数组,然后我们将建立几个简单的字符串数组来保存这首诗:
func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String { return (to + inNewTextLineArray).joined(separator: "\n")}let originalVersionPartOne = ["我遇到了一个来自古色古香之地的旅行者“,”谁说:两条巨大而无躯干的石头腿”,“站在沙漠中。靠近他们,在沙滩上,”,“半沉,一个破碎的脸庞,皱着眉头,”,“和皱巴巴的嘴唇,和冷冷的命令的冷笑,”,“告诉它的雕刻家那些激情读得很好”,“它们仍然存在,印在这些没有生命的东西上,”,“嘲笑他们的手和喂养的心:”,“在基座上这些词出现了:","\"我的名字是奥兹曼迪亚斯,万王之王:","看看我的作品吧,强大的,绝望的!\""]let originalVersionPartTwo = ["没有剩下的东西。围绕衰败", “那巨大的残骸,无边无际,光秃秃的”,“孤零零的沙子伸向远处。”]var fullPoem = appendTextLines(originalVersionPartTwo, to: originalVersionPartOne)print(fullPoem) 我遇到了一个来自古国的旅人,他说:二巨大而没有躯干的石腿矗立在沙漠中。在他们附近,在沙滩上,半沉,一个破碎的脸庞,皱着眉,皱起的嘴唇,冷笑的命令,告诉它的雕刻家那些激情读到的,但仍然存在,印在这些没有生命的东西上,嘲笑他们的手和饱食的心:在基座上出现了这些话:“我的名字是奥兹曼迪亚斯,万王之王:看看我的作品,你们强大的,绝望的!”除此之外什么都没有了。在那巨大残骸的腐烂周围,无边无际而光秃秃的,孤独而平坦的沙子向远处延伸。这一切都相当简单,只要查看代码,我们就可以很容易地知道发生了什么,但让我们假设我们不能。我使用 Doxygen/Apple Markdown 语法来记录我的函数,并且我总是记录我的函数。 /// 这是一个函数。 /// 函数做事。 /// 这个函数做的东西。func functionThatDoesStuff() { } /** 这是一个函数。函数做事。这个函数做事。 */func functionThatDoesStuff() { } 需要注意的是“///”或“/* *”的使用。第三个正斜杠或第二个星号表示应解析以下注释以进行自动记录。如果这些不存在,Doxygen、Jazzy 或 Apple Markdown Parser 将忽略注释。
我再往前走一步。我还想添加一个“视觉划分”键。也就是说,一行字符提供了一个可视化队列,告诉快速滚动的读者一个函数(或数据结构)在那里。我倾向于使用井号或星号行,就像这样:/* ################################ ################# *//** 这是一个函数。函数做事。这个函数做的事情。*/func functionThatDoesStuff() { } 那行没有实际用途。它不会被 doc 生成器解析,而是在文件中添加了另一行。但它提供了一个视觉队列,说“这里有一个函数/方法。”由于我经常快速浏览文件,因此这些文件非常宝贵。它们帮助我避免搜索一页代码和括号内的上下文,寻找函数声明。因此,考虑到这一点,让我们为简单的 append 函数添加一个函数头:/* ############################ ##################### *//** 此函数会将一个字符串数组附加到另一个字符串,然后将整个复合数组作为单个字符串返回,由换行符连接。 */func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String { return (to + inNewTextLineArray).joined(separator: "\n")}解释函数的作用。
使用 Doxygen 和 Apple Markdown 语法,我们可以格式化该函数头文档以键入解析器。描述这些队列的确切语法有点超出本文的范围,但这里是 Doxygen 手册,这里是对 Apple 系统的一个很好的描述。在我的示例中,我将使用 Swift 和 Apple Markdown 语法(也可以由 Doxygen 解析)。一件好事,就是记录每个参数,这样 autodoc 系统会将它们显示为单独的块。 Apple 规定的方法是为列表创建 Markdown,并使用“参数”关键字作为每个描述的前言,如下所示:/* ################## ############################### *//** 此函数会将一个字符串数组附加到另一个字符串,然后将整个复合数组作为单个字符串返回,由换行符连接。 - 参数 inNewTextLineArray:将附加到数组末尾的字符串数组。 - 参数为:将在前一个数组之前的字符串数组。 */func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String { return (to + inNewTextLineArray).joined(separator: "\n")} 如果我们再选择函数名,同时Quick Help Inspector 在右侧边栏中打开,我们将看到:
稍后,我将展示 Jazzy 将如何记录它(这很酷)。我们还可以使用一些 Markdown 来定义函数返回的内容,并且还允许我们改进基本的“模糊”:/* ################# ################################ *//** 此函数会将一个字符串数组附加到另一个字符串,返回一个简单的连接字符串。 - 参数 inNewTextLineArray:将附加到数组末尾的字符串数组。 - 参数为:将在前一个数组之前的字符串数组。 - 返回:整个复合数组作为单个字符串,由换行符连接。 */func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String { return (to + inNewTextLineArray).joined(separator: "\n")} 最后,只是为了让自己与苹果的推荐保持一致(我有时遵循,有时不遵循):/* ################################## ############### *//** 此函数将一个字符串数组附加到另一个字符串,返回一个简单的连接字符串。 - 参数: - inNewTextLineArray:将附加到数组末尾的字符串数组。 - to:将在前一个数组之前的字符串数组。 - 返回:整个复合数组作为单个字符串,由换行符连接。 */func appendTextLines(_ inNewTextLineArray: [String], to: [String]) -> String { return (to + inNewTextLineArray).joined(separator: "\n")} 我已经这样做了这么久,它是基本上是第二天性。当我创建一个函数/方法时,我会立即添加一个空标题,如下所示:至少,即使我永远不会回去填写标题,它也会提供“可视队列”,所以我会知道那里有一个函数声明.
大多数语言都有“MARK”注释的一些变体。这是文本编辑器(和自动文档生成器)插入某种“中断”的队列。我倾向于在我的 Swift 工作中使用“MARK”注释,因为它最灵活和健壮(我也将所有警告作为错误运行,所以我不喜欢生成警告的代码注释,但那是另一回事)。为了展示我们如何使用 MARK 注释,让我们添加这首诗的另一个版本:/* ############################ ###################### *//** 此函数将附加......