4  Fundamental development workflows

Chapter 3 中了解了 R 包和库的底层之后,我们在这里提供了创建包并将其移动到开发过程中出现的不同状态的基本工作流程。

4.1 Create a package

4.1.1 Survey the existing landscape

许多程序包都是由于一个人对一些本应更容易完成的普通任务感到沮丧而产生的。 您应该如何判断某些东西是否值得制作为程序包呢? 虽然这个问题没有明确的答案,但了解至少两种类型的回报对您是有帮助的:

  • 结果(Product):当这个功能正式实现时,您的工作生活会变得更好。
  • 过程(Progress):更好地掌握 R 会使您的工作更有效率。

如果您只关心结果带来的好处,那么您的主要目标就是在现有的程序包中探索。 Silge,Nash 和 Graves 在 useR! 2017 组织了一次调查和会议。 他们为 R Journal (Silge, Nash, and Graves 2018) 撰写的文章提供了全面的资源综述。

如果您正在寻找方法来提高对 R 的掌握,那么您仍然应该对 R 的运行环境多加了解。 但是,即使有相关的前期工作,也有很多充分的理由来制作自己的程序包。 专家们的方式是通过实践来为许多功能构建程序包,通常是非常基本的功能,并且您应该有同样的机会通过修补来学习。 如果您只被允许做一些从未接触过的事情,那么您可能会遇到一些非常模糊或非常困难的问题。

根据用户界面、默认值和在极端情况下的表现来评估现有工具的适用性也是有效的。 如果一个程序包在技术上可以满足您的需要,但是对于您的用例来说非常不符合舒适正确的使用方式,那么仍然可以说它不能满足您的需求。 在这种情况下,您可能需要开发自己的实现或编写平滑尖锐边缘的包装函数。

如果您的工作属于明确定义的领域,请了解现有的 R 包,即使您已决定创建自己的包。 他们是否遵循特定的设计模式? 是否有作为主要输入和输出的通用特定数据结构? 例如,围绕空间数据分析 (r-spatial.org) 有一个非常活跃的 R 社区,它已经成功地自我组织,以促进不同维护者包之间的更大一致性。 在建模中,hardhat package 提供了脚手架,用于创建与 tidymodels 生态系统配合良好的建模包。 如果你的包很好地融入周围的环境,它会得到更多的使用并且需要更少的文档。

4.1.2 Name your package

“There are only two hard things in Computer Science: cache invalidation and naming things.”

— Phil Karlton

在创建程序包之前,您需要为它取一个名字。 这可能是创建程序包的过程中最困难的部分! (尤其是因为没有人可以为您实现取名的自动化。)

4.1.2.1 Formal requirements

有三个正式的要求:

  1. 名称只能由字母、数字和句点组成,即 .
  2. 它必须以字母开头。
  3. 它不能以句点结尾。

不幸的是,这意味着您不能在您的包名中使用连字符或下划线,即 -_。 我们建议不要在包名中使用句点,因为这会混淆句点与文件扩展名和 S3 方法的关联。

4.1.2.2 Things to consider

如果您打算和别人分享您的程序包,那么花几分钟想一个好名字是值得的。 以下是一些需要考虑的事项:

  • 选择一个便于 Google 搜索的独特名称。 这使得潜在的用户能够很容易地找到您的程序包(以及相关的资源),并能让您看到是谁在使用它。

  • 不要选择一个已经在 CRAN 或 Bioconductor 上使用的包名。 您可能还需要考虑一些其他类型的命名冲突:

    • 是否有在 GitHub 上成熟的且正在开发中的程序包,该程序包已经有了一定的历史,并且似乎即将发布?
    • 这个名称是否已经用于另一个软件,例如是 Python 或 JavaScript 生态系统中的库或框架?
  • 避免同时使用大写和小写字母:这样做会使包名称难以键入,甚至难以记住。 例如,很难记住一个程序包叫做 Rgtk2 还是 RGTK2 或 RGtk2。

  • 优先选择可发音的名字,这样人们在谈论你的程序包时会很舒服,并且能够在他们的脑海里听到它。

  • 找到一个能唤起对问题的联想的单词,并对其进行修改,使其具有唯一性。 以下是一些示例:

    • lubridate 使日期和时间更容易。
    • rvest 从网页中“收获”内容。
    • r2d3 提供了使用 D3 可视化的实用程序。
    • forcats 是因子(factors)的变位词,我们用它来表示分类数据(for categorical data)。
  • 使用缩写,如下所示:

    • Rcpp = R + C++ (plus plus)
    • brms = Bayesian Regression Models using Stan
  • 名字后添加额外的字符 R:

    • stringr 提供字符串工具。
    • beepr 播放通知声音。
    • callr 从 R 调用 R。
  • 别被起诉。

    • 如果您要创建一个与商业服务交互的包,请查看商标使用指南。例如,rDrop 不被称为 rDropbox,因为 Dropbox 禁止任何应用程序使用完整的商标名。

Nick Tierney 在他的 Naming Things 博客文章中展示了一个有趣的程序包名称类型学;请参阅该文章以获取更多鼓舞人心的示例。 他也有一些重命名包的经验,因此,如果您第一次取名没有做对,他的博客文章 So, you’ve decided to change your r package name 将是一个很好的资源。

4.1.2.3 Use the available package

同时遵守上述所有建议是十分困难的,因此您显然需要做出一些权衡。 available 程序包中有一个名为 available() 的函数,可以帮助您从多个角度评估可能的程序包名称:

library(available)

available("doofus")
#> Urban Dictionary can contain potentially offensive results,
#>   should they be included? [Y]es / [N]o:
#> 1: 1
#> ── doofus ──────────────────────────────────────────────────────────────────
#> Name valid: ✔
#> Available on CRAN: ✔ 
#> Available on Bioconductor: ✔
#> Available on GitHub:  ✔ 
#> Abbreviations: http://www.abbreviations.com/doofus
#> Wikipedia: https://en.wikipedia.org/wiki/doofus
#> Wiktionary: https://en.wiktionary.org/wiki/doofus
#> Sentiment:???

available::available() 执行以下操作:

  • 检查有效性。
  • 检查在 CRAN、Bioconductor 和其他产品上的可用性。
  • 搜索各种网站,帮助您发现任何意料之外的含义。在交互式会话中,您在上面看到的 URLs 将在浏览器选项卡中打开。
  • 试图报告该名称是否有积极情绪或消极情绪。

pak::pkg_name_check() 是具有类似目的的替代函数。 由于 pak 包的开发比现有的更积极,它可能会成为未来更好的选择。

4.1.3 Package creation

为程序包命名后,有两种创建程序包的方法:

这将产生最小的可工作的程序包,它包含三个组件:

  1. 一个 R/ 目录,您将在 Chapter 6 中了解到具体内容。

  2. 一个基础的 DESCRIPTION 文件,您将在 Chapter 9 中了解到具体内容。

  3. 一个基础的 NAMESPACE 文件,您将在 Section 10.2.2 中了解到具体内容。

它也可能包含一个 RStudio 项目文件,pkgname.Rproj,这使您的程序包易于与 RStudio 一起使用,如下所述。 基础的 .Rbuildignore.gitignore 文件也被包含在目录中。

Warning

不要使用 package.skeleton() 创建程序包。 因为这个函数与 R 一起提供,您可能会想使用它,但是它会创建一个在调用 R CMD build 时立刻抛出错误的程序包。 它期望的开发过程与我们在这里使用的不同,所以修复这个损坏的初始状态只会让使用 devtools(尤其是 roxygen2)的人做不必要的工作。 请使用 create_package()

4.1.4 Where should you create_package()?

create_package() 的主要且唯一必需的参数是新程序包所在的 path

create_package("path/to/package/pkgname")

请记住,这是您的程序包在源代码(source)形式(Section 3.2)时所处的位置,而不是已安装(installed)形式(Section 3.5)。 已安装的包位于库(library)中,我们在 Section 3.7 中讨论了库的常规设置。

源码包应该放在哪里? 主要原则是该位置应该与已安装包所在的位置不同。 在没有其他外部考虑的情况下,典型的用户应该在其主目录中为 R(源代码)包指定一个目录。 我们与同事讨论过这一点,您最喜欢的一些 R 包的源代码位于 ~/rrr/~/documents/tidyverse/~/R/packages/~/pkg/ 等目录中。 我们中的一些人使用一个目录来实现这一点,其他人则根据他们的开发角色(contributor vs. not)、GitHub 组织(tidyverse vs r-lib)、开发阶段(active vs. not)等将源码包划分为几个目录。

以上内容可能反映出我们主要是工具构建者。 学术研究人员可能会围绕单个出版物组织他们的文件,而数据科学家可能会围绕数据产品和报告来组织。 对于每一种特定的方法,没有特定的技术或传统原因来说明为何要选择它。 只要在源码包和已安装的包之间保持清晰的区别,仅仅需要选择一种在整个系统中有效的文件组织策略,并始终如一地使用它即可。

4.2 RStudio Projects

devtools 与 RStudio,一个我们相信是大多数 R 用户的最佳开发环境联系紧密、携手合作。 明确地说,您可以使用 devtools 而不使用 RStudio,也可以在 RStudio 中开发程序包而不使用 devtools。 但是这种特殊的、双向的关系使得一起使用 devtools 和 RStudio 变得非常有意义。

RStudio

一个 RStudio Project,包含一个大写字母 “P”,是您计算机上的一个常规目录,其中包含一些(大部分是隐藏的)RStudio 基础文件,以便您在一个或多个 projects 上工作,带有小写字母 “p”。 一个 project 可以是一个 R 包、一个数据分析报告、一个 Shiny app、一本书、一个博客等等。

4.2.1 Benefits of RStudio Projects

Section 3.2 中,您已经知道源码包位于您计算机上的目录中。 我们强烈建议将每个源码包作为一个 RStudio 项目。 以下是这样做的好处:

  • 项目是非常可启动的(“launch-able”)。 文件浏览器和工作目录将完全按照您需要的方式进行设置,马上可以开始工作,从而很容易在一个项目中启动一个新的 RStudio 实例。

  • 每个项目都是独立的;在一个项目中运行的代码不会影响任何其他项目。

    • 您可以同时打开多个 RStudio 项目,并且在项目 A 中执行的代码不会对项目 B 的 R session 和工作区(workspace)产生任何影响。
  • 您可以使用方便的代码导航工具,如 F2 跳转到函数定义,Ctrl + . 来按名称查找函数或文件。

  • 您可以使用很有帮助的键盘快捷键和可点击的界面,以执行常见的程序包开发任务,如生成文档、运行测试或检查整个程序包。

    Screenshot of an RStudio window with a semi-transparent black box displaying keyboard shortcuts that overlays a majority of the window.

    Figure 4.1: Keyboard Shortcut Quick Reference in RStudio.

RStudio

查看最有用的键盘快捷键,请按 Alt + Shift + K,或者使用 Help > Keyboard Shortcuts Help。 您应该会看到类似 Figure 4.1 的内容。

RStudio 还提供 Command Palette,它可以快速、可搜索地访问所有 IDE 的命令 – 当您不记得特定的键盘快捷键时尤其有用。 它通过 Ctrl + Shift + P(Windows 和 Linux)或 Cmd + Shift + P(macOS)调用。

RStudio

在 Twitter 上关注 @rstudiotips,定期获取 RStudio 提示和技巧。

4.2.2 How to get an RStudio Project

如果您按照我们的建议使用 create_package() 创建新的程序包,那么这会自行解决。 如果你在 RStudio 工作,每个新程序包也将是一个 RStudio 项目。

如果您需要将预先存在的源码包的目录指定为 RStudio 项目,请选择以下选项之一:

  • 在 RStudio 中,执行 File > New Project > Existing Directory
  • 使用预先存在的 R 源码包的路径调用 create_package()
  • 调用 usethis::use_rstudio(),将 active usethis project 设置为现有的 R 包。 实际上,这可能意味着您只需要确保工作目录在已经存在的程序包中。

4.2.3 What makes an RStudio Project?

RStudio 项目的目录将包含一个 .Rproj 文件。 通常,如果目录名为“foo”,则项目文件为 foo.Rproj。 如果这个目录也是一个 R 包,那么包名通常也是 “foo”。 故障最少的方法是使所有这些名称一致,并且不要将程序包嵌套在项目内的子目录中。 如果您决定采用其他工作流程,那么可能会让您觉得您在与工具进行不必要的争斗。

.Rproj 文件只是一个文本文件。 以下是 usethis 使用的默认项目文件:

Version: 1.0

RestoreWorkspace: No
SaveWorkspace: No
AlwaysSaveHistory: Default

EnableCodeIndexing: Yes
Encoding: UTF-8

AutoAppendNewline: Yes
StripTrailingWhitespace: Yes
LineEndingConversion: Posix

BuildType: Package
PackageUseDevtools: Yes
PackageInstallArgs: --no-multiarch --with-keep.source
PackageRoxygenize: rd,collate,namespace

您不需要手动修改这个文件。 可以通过 Tools > Project Options (Figure 4.2) 或者右上角 Project 菜单栏中的 Project Options (Figure 4.3) 提供的界面进行编辑。

The Project Options preference page in the RStudio IDE. On the left side are nine categories: General, Code Editing, R Markdown, Python, Sweave, Spelling, Build Tools, Git/SVN, and Environments. The General category is selected. In the main part of the window are three options with dropdown boxes: 1. Restore .RData into workspace at startup; 2. Save workspace to .RData on exit; and 3. Always save history (even if not saving .RData). All three of these options are set to "(Default)". There are two options with checkboxes: 1. Disable .Rprofile execution on session start/resume 2. Quit child processes on exit Both of these are unchecked. At the top of the window is the statement: "Use (Default) to inherit the global default setting".

Figure 4.2: Project Options in RStudio.

Image of the RStudio IDE Projects drop-down menu, with items: "New Project...", "Open Project...", "Open Project in New Session...",  "Close Project", then a list of projects the user has had open  recently, "Close Project", and "Project Options...".  The "Project Options..." item is selected.

Figure 4.3: Projects Menu in RStudio.

4.2.4 How to launch an RStudio Project

在 macOS 的 Finder 或 Windows 资源管理器中双击 foo.Rproj 文件,以便在 RStudio 中启动 foo Project。

您也可以通过 File > Open Project (in New Session) 或右上角的 Project 菜单从 RStudio 中启动 Projects。

如果您使用的是一个生产力应用程序或启动器应用程序,您可能可以将其配置为对 .Rproj 文件执行一些令人愉快的操作。 我们都使用 Alfred 来实现这一点1,只有 macOS 有该工具,但是 Windows 也有类似的工具。 事实上,这是一个非常好的理由首选使用生产力应用程序。

一次性打开多个项目是非常正常的而且富有成效!

4.2.5 RStudio Project vs. active usethis project

您会注意到,大多数 usethis 函数不使用路径:它们对 “active usethis project” 中的文件进行操作。 usethis 程序包假设以下这些在 95% 的时间内都是一致的:

  • 当前的 RStudio Project,如果使用 RStudio。
  • 活跃的 usethat 项目。
  • R 进程的当前工作目录。

如果事情看起来很奇怪,可以调用 proj_sitrep() 来获取“情况报告”。 这将识别出一些特殊情况,并提出如何回到更良好的状态。

# these should usually be the same (or unset)
proj_sitrep()
#> *   working_directory: '/Users/jenny/rrr/readxl'
#> * active_usethis_proj: '/Users/jenny/rrr/readxl'
#> * active_rstudio_proj: '/Users/jenny/rrr/readxl'

4.3 Working directory and filepath discipline

在开发包时,您将会执行 R 代码。 这将是工作流调用(例如 document()test())和帮助您编写函数、示例和测试的特殊(ad hoc)调用的混合。 我们强烈建议您将 R 进程的工作目录设置为源码包的顶层目录。 这通常会默认发生,所以这确实是一个建议,可以避免需要您摆弄工作目录的开发工作流程。

如果您在程序包开发方面毫无经验,那么您没有太多的基础来支持或抵制此建议。 但那些有经验的人可能会觉得有些不安。 在子目录中工作时,我们应该如何表示路径,比如 tests/? 当它变得与我们的工作相关时,我们将向您展示如何利用路径构建帮助器,例如 testthat::test_path(),它会在执行时确定路径。

它的基本思想是,通过不使用工作目录,鼓励您编写能够明确表达意图的路径(“从测试目录中读取 foo.csv),而不是隐式的表达(“从当前的工作目录中读取 foo.csv,我认为该目录将是测试目录)。 依赖隐式路径的一个可靠迹象就是不断地修改工作目录,因为您正在使用 setwd() 手动实现路径中隐含的假设。

这种思想可以消除所有路径问题,让日常的开发变得更加愉快。 隐式路径难以正确设置的原因有两个:

  • 回想一下在开发周期中程序包可以采用的不同形式(Chapter 3)。 在这些状态中,存在哪些文件和文件夹以及它们在层次结构中的相对位置都是互不相同的。 编写满足所有程序包状态的相对路径是很困难的。
  • 最终,您的程序包将由您和 CRAN 使用内置工具(built-in tools)处理,如 R CMD buildR CMD checkR CMD INSTALL。 很难跟踪这些过程中每个阶段的工作目录是什么。

testthat::test_path()fs::path_package()rprojroot package 这样的路径帮助器对于构建弹性的路径非常有用,这些路径可以在开发和使用过程中出现的所有情况下都有效。 消除脆弱路径的另一种方法是严格使用在程序包中存储数据的适当方法(Chapter 7),并在适当的时候采用会话(session)的临时目录,例如针对短暂的测试工件(ephemeral testing artefacts)(Chapter 13)。

4.4 Test drive with load_all()

load_all() 函数无疑是 devtools 工作流中最重要的部分。

# with devtools attached and
# working directory set to top-level of your source package ...

load_all()

# ... now experiment with the functions in your package

load_all() 是在 “lather, rinse, repeat” 这一程序包开发周期中的关键步骤:

  1. 调整函数定义。
  2. load_all()
  3. 通过运行一个较小的示例或一些测试来尝试更改。

当您刚接触程序包开发或 devtools 时,很容易忽视 load_all() 的重要性,并在数据分析工作流中养成一些不合适的习惯。

4.4.1 Benefits of load_all()

当您第一次开始使用开发环境,如 RStudio 或 VS Code 时,最大的便利之处是能够从 .R 脚本中发送代码到 R 控制台(R console)中执行。 这种流动性使得遵循将源代码视为真实存在(而不是工作区中的对象)和保存 .R 文件(而不是保存和重新加载 .Rdata)的最佳做法是可以接受的。

load_all() 对于程序包开发来说有着同样的意义,相反的是,它要求您不要像脚本代码那样测试程序包代码。 load_all() 模拟查看源代码更改效果的完整过程,这是一个非常笨重的过程,您不会希望经常这样做。 Figure 4.4 强调了 library() 函数只能加载已安装的包,而 load_all() 基于当前包源对此进行了高保真模拟。

Diagram listing five package states: source, bundle, binary, installed, in memory. Two functions for converting a package from one state to another are depicted. First, `devtools::load_all()` is shown to convert a source package to one that is in memory. Second, `library()` is shown to put an installed package in memory.

Figure 4.4: devtools::load_all() vs. library().

load_all() 的主要优点有:

  • 您可以快速迭代,这将鼓励探索和渐进式开发过程。

    • 这种迭代加速对于具有编译代码的程序包来说尤其显著。
  • 您可以在命名空间机制下进行交互开发,该机制准确模拟了其他人使用您的已安装程序包时的情况:

    • 您可以直接调用自己的内部函数,而不必使用 :::,也不必在全局工作区中临时定义函数。
    • 您还可以从导入到 NAMESPACE 的其他程序包中调用函数,而不必试图通过 library() 附加这些依赖项。

load_all() 消除了开发工作流中的麻烦,也消除了使用替代方法的诱惑,该替代方法通常会导致命名空间和依赖项管理方面的错误。

4.4.2 Other ways to call load_all()

在程序包 Project 中工作时,RStudio 提供了几种调用 load_all() 的方法:

  • 键盘快捷键:Cmd + Shift + L (macOS)、Ctrl + Shift + L (Windows, Linux)
  • Build 窗格的 More … 菜单
  • Build > Load All

devtools::load_all()pkgload::load_all() 的一个简单封装,它增加了一点用户友好性。 您不太可能以编程的方式或在另一个程序包中使用 load_all(),但如果您这样做了,您可能应该直接使用 pkgload::load_all()

4.5 check() and R CMD check

Base R 提供各种命令行工具,R CMD check 是检查 R 包是否有效的官方方法。 如果你打算将你的包提交给 CRAN,那么通过 R CMD check 是必不可少的,但我们强烈建议你自己遵守这个标准,即使你不打算在 CRAN 上发布你的包。 R CMD check 可以检测到许多您很难发现的常见问题。

我们推荐的运行 R CMD check 的方法是在 R 控制台中通过 devtools:

devtools::check()

我们推荐这样做是因为它允许您从 R 内部运行 R CMD check,这会显着减少摩擦并增加您尽早和经常 check() 的可能性! 这种对流动性和快速反馈的强调与 load_all() 的动机完全相同。 对于 check(),它实际上是在为您执行 R CMD check。 这不仅仅是一个高保真模拟,load_all() 就是这样。

RStudio

RStudio 在 Build 菜单、Build 窗格中通过 Check 以及键盘快捷键 Ctrl + Shift + E(Windows 和 Linux)或 Cmd + Shift + E(macOS)公开 check()

我们在新包开发人员中经常看到的一个菜鸟错误是在运行 R CMD check 之前对他们的包做太多工作。 然后,当他们最终运行它时,通常会发现很多问题,这会让人非常沮丧。 这是违反直觉的,但减少这种痛苦的关键是更频繁地运行 R CMD check:越早发现问题,就越容易修复2。 我们在 Chapter 1 中非常有意地模拟了这种行为。

这种方法的上限是每次进行更改时都运行 R CMD check。 我们不会经常手动运行 check(),但是当我们积极处理一个包时,通常每天多次运行 check() 。 不要在几天、几周或几个月内修补你的包,等待一些特殊的里程碑来最终运行 R CMD check。 如果您使用 GitHub(Section 20.1),我们将向您展示如何进行设置,以便在您每次推送时自动运行 R CMD checkSection 20.2.1)。

4.5.1 Workflow

这是 devtools::check() 内部发生的事情:

  • 通过运行 devtools::document() 确保文档是最新的。

  • 在检查之前捆绑包(Section 3.3)。 这是检查包的最佳实践,因为它确保检查从一个干净的状态开始:因为捆绑包不包含任何可以在源包中累积的临时文件,例如编译代码附带的 .so.o 文件,您可以避免此类文件将生成的虚假警告。

  • NOT_CRAN 环境变量设置为“true”。 这允许您有选择地跳过 CRAN 上的测试。 有关详细信息,请参阅 ?testthat::skip_on_cranSection 15.4.1

检查包的工作流程很简单,但很乏味:

  1. 运行 devtools::check(),或按 Ctrl/Cmd + Shift + E。

  2. 解决第一个问题。

  3. 重复直到没有问题为止。

R CMD check 返回三种类型的消息:

  • ERRORs:无论您是否提交给 CRAN,都应该解决的严重问题。

  • WARNINGs:如果您打算提交给 CRAN,则可能必须解决的问题(即使您不打算提交也是一个好主意)。

  • NOTEs:轻微的问题,或者在少数情况下,只是一个观察。 如果你提交给 CRAN,你应该努力消除所有的 NOTEs,即使它们是误报。 如果您没有 NOTEs,则不需要人为干预,并且包提交过程会更容易。 如果无法消除 NOTEs,您需要在提交评论中说明可以的原因,如 Section 22.7 所述。 如果您不提交给 CRAN,请仔细阅读每条注意事项。 如果消除 NOTE 很容易,那是值得的,这样您就可以继续争取完全干净的结果。 但是,如果删除 NOTE 会对您的包产生净负面影响,那么容忍它是合理的。 确保这不会导致您忽略其他真正应该解决的问题。

R CMD check 由几十个单独的检查组成,在这里一一列举会让人不知所措。 有关详细信息,请参阅我们的 online-only guide to R CMD check

4.5.2 Background on R CMD check

随着您积累包开发经验,您可能希望在某些时候直接访问 R CMD check。 请记住,R CMD check 是您必须在终端而不是 R 控制台中运行的东西。 您可以这样查看其文档:

R CMD check --help

R CMD check 可以在以源码形式(Section 3.2)保存 R 包的目录上运行,或者最好在捆绑包(Section 3.3)上运行:

R CMD build somepackage
R CMD check somepackage_0.0.0.9000.tar.gz  

要了解更多信息,请参阅 Writing R ExtensionsChecking packages


  1. 具体来说,我们将 Alfred 配置为在建议打开应用程序或文件时在其搜索结果中优先选择 .Rproj 文件。 要向 Alfred 注册 .Rproj 文件类型,请转至 Preferences > Features > Default Results > Advanced。 将任何 .Rproj 文件拖到此空间中,然后关闭。↩︎

  2. Martin Fowler 撰写的 FrequencyReducesDifficulty 是一篇很棒的博客文章,提倡“如果有困难,就多做几次”。↩︎