1. 注释

在现有的编程语言中,注释有多种用途。主要的用例包括:

  • 文档:为API的用户和未来的维护者提供人类可读的注释,解释其功能以及如何使用它。这种注释通常附加在函数声明、类定义、公共成员声明、文件范围等API的相似粒度级别上。
1
2
3
4
5
6
7
/// 一个连接小部件集合的容器。
class WidgetAssembly {
    /// 如果可能的话,改善组件的外观。
    void decorate(bool repaint_all = false);

    // ...
};
  • 实现注释:为代码的未来读者或维护者提供人类可读的注释,解释意图和机制,或总结代码的行为,以避免读者或维护者需要详细阅读它。当这些细节从代码本身可能不容易看出或可能需要非平凡的工作来推断时,通常使用这种注释,而且这种注释往往很短。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void WidgetAssembly::decorate(bool repaint_all) {
    // ...

    // 绘制上次更改后的所有小部件。
    for (auto &w : widgets) {
    if (repaint_all || w.modified > last_foo)
        w.paint();
    }
    last_decorate = now();

    // ...
}
  • 语法消歧注释:包含代码或伪代码的注释,旨在让人类读者更容易地按照编译器的方式解析代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void WidgetAssembly::decorate(bool repaint_all /*= false*/) {
// ...

/*static*/ std::unique_ptr<WidgetAssembly> WidgetAssembly::make() {
// ...

assembly.decorate(/*repaint_all=*/true);
// ...

}  // 结束命名空间 WidgetLibrary
  • 禁用的代码:包含已被禁用的代码区域的注释,因为代码不完整或不正确,或为了在调试时隔离问题,或作为正在进行的更改的参考材料。通常认为将这样的注释加入版本控制是不良实践。

1.1 背景

在C++中,实际上有三种不同的方式来表示注释:

1.1.1 行注释

在C++中,单行注释(有时是多行注释)使用// ...表示:

1
2
// 下一行声明了一个变量。
int n; // 这是关于'n'的注释。

(这些有时被称为"BCPL注释"。)

  • 可以出现在任何地方(在一行的开头或在标记后)。
  • 可以包含任何文本(除了换行符)。
  • 在逻辑行的末尾结束。
  • 通过在注释末尾添加\(或在C++14及更早版本中的??/)来继续。
  • 与非注释语法无歧义。
  • “嵌套”,即//内的//无效。
  • 不与其他类型的注释嵌套。

这种注释语法经常用来表示文档(有时使用Doxygen风格的///引入符)和实现注释。

1.1.2 块注释

在C++中,行内注释(有时是多行注释)使用/*...*/表示:

1
f(/*size*/5, /*initial value*/1);
  • 可以出现在任何地方(在一行的开头或在标记后)。
  • 可以包含任何文本(除了*/)。
  • */分隔符处结束(该分隔符可能由\行继续分隔)。
  • 与非注释语法有歧义:int a=1, *b=&a, c=a/*b;,尽管这在实践中不是问题。
  • 不嵌套——第一个*/结束了注释。

这种注释语法经常用来表示语法消歧注释,并有时用于禁用的代码。某些编码风格还使用这种注释风格来表示较长的文档注释(有时使用Doxygen风格的/**引入符)。

1.1.3 #if 0

在C++程序中,代码块经常使用#if 0注释掉:

1
2
3
#if 0
int n;
#endif
  • 只能出现在逻辑行的开头。
  • 只能包含预处理标记序列(包括无效标记,如',但不包括未终止的多行字符串字面量)。
  • 在匹配的#endif分隔符处结束。
  • 与任何其他语法无歧义。
  • 嵌套得当,并且可以在其中嵌套其他种类的注释。

这种语法通常只用于禁用的代码。

1.2 详细内容

1.2.1 注释概述

_注释_是一个词法元素,以//字符开始并运行到行的末尾。我们没有物理行继续的机制,所以尾随的\不会将注释扩展到后续的行。

试验: 在引入注释的//字符之前不能有其他文本,除了水平空白。要么整行都是注释,要么一点也不是。

//后的字符必须是一个空白字符。换行符是一个空白字符,所以只包含//的行是一个有效的注释。文件的结尾也构成空白。

在形成标记之前,所有注释都被删除。

示例:

1
2
3
4
// 这是一个注释,会被忽略。\
这不是一个注释。

var Int: x; // 错误,不允许尾随注释

1.2.2 块注释

试验: 不提供块注释的支持。通过注释掉区域中的每一行来注释掉大量的人类可读的文本或代码。

块注释的理由

对于实现注释的用例,支持块注释的价值不大。我们期望这样的注释通常很短,而在现有的C++代码库中,如果有长的实现注释,通常使用的是行注释而不是块注释。因此,由于我们认为文档用例超出了范围,并且打算通过语言语法解决语法消歧用例,块注释的唯一目的就是禁用代码。块注释可以为行内禁用的代码和多行禁用的代码提供更方便的支持。

现有的块注释语法不适合禁用代码的用例。C++中的/* ... */块注释不嵌套,并且不能用于可靠地注释掉一个代码块,因为它可以被//注释或字符串字面量中的*/终止。#if 0 ... #endif语法不适合Carbon,因为我们不打算通常有一个预处理器,而且需要文本之间的内容由大部分有效的标记序列组成,不允许某些形式的不完整代码。

我们应该不情愿地发明新东西:为了禁用代码的短暂和罕见的用例,很难证明引入新的语法的成本是合理的。同样,我们应该不情愿地使用现有的语法与新的语义,如一个/* ... */注释,它标记其内容,以避免对C++开发者的惊讶。

禁用代码的用例可以用行注释来解决,通过注释掉预期区域中的每一行,并在禁用行内的代码时重新排列或复制行。这可能很麻烦,但不清楚这种负担是否足以证明将另一种形式的注释引入到语言中。通过不提供这样的注释形式,我们的目标是发现结果摩擦是否证明了语言的增加。

1.2.3 保留的注释

//字符后面不跟随空白的注释是为未来的扩展保留的。预期的可能的扩展是块注释、文档注释和代码折叠区域标记。

保留的注释的理由

我们预期将来可能会增加其他种类的注释。在注释语法中保留语法空间,以便程序容易避免,允许我们将这样的额外注释作为非破坏性的变更添加。

1.3 考虑的替代方案

1.3.1 行内注释

我们可以包括一个类似于C风格块注释的功能,作为提供附加到小于一行的程序元素的注释的方式。在C++代码中,这样的注释经常用于注释函数参数名称和类似的语法消歧用例:

1
render(/*use_world_coords=*/true, /*draw_frame=*/false);

我们期望这些用例将由Carbon的语法扩展来解决,例如通过添加命名参数或注释语法,以允许这样的话语以代码而不是注释的形式表示,所以它们对Carbon程序员和Carbon语言工具都是有意义的。

我们可以允许在包含其他内容的行上的尾部注释。这样的注释在我们的样本C++语料库中经常用于描述同一行上的实体、标签或闭合括号的含义:

1
2
3
4
5
6
7
namespace N {
int n; // number of hats
enum Mode {
  mode1, // first mode
  mode2 // second mode
};
} // end namespace N

除了最后一种情况,我们期望将注释移到声明之前是合理的。“end namespace"注释的情况是另一个语法消歧用例,我们期望通过语法更改来解决。总的来说,我们应该避免任何需要消歧注释的语法,要么通过将这些注释提升到语言语法,要么通过更改语法直到注释不再需要,例如不为描述命名空间和包的内容的大范围提供一个分隔的范围语法。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 这声明了命名空间N,但没有打开一个范围。
namespace N;

// 这声明了命名空间N的一个成员。
@"Number of hats."
var Int: N.n;

enum N.Mode {
  @"First mode."
  mode1;
  @"Second mode."
  mode2;
}

行内注释对于代码格式化工具来说是一个挑战,它们需要理解注释"附加到"什么程序语法上,以便正确地将注释与代码重新排列。这种关注通过要求注释始终在自己的行上被缓解,但并没有完全消除。我们可以通过使用方向标记在注释中来允许行内注释,同时仍然保留一些关于注释如何附加的想法:

1
2
3
4
match (x) {
  case .Foo(1, 2, //> either 3 or 4 >// Int: n) => { ... }
  case .Foo(2, Int: n //< either 3 or 4 <//, 5) => { ... }
}

即使有了理解注释如何附加的方法,行包装这样的注释也是一个复杂的挑战。例如,在多行上有对齐的尾部注释的情况下,格式化需要特殊处理:

1
2
3
var Int: quality = 3;   // The quality of the widget. It should always
                        // be between 1 and 9.
var Int: blueness = 72; // The blueness of the widget, as a percentage.

在这里,一个将blueness重命名为blue_percent的工具可能需要重新排列quality后面的注释以及blueness后面的注释。此外,如果最后一行变得太长,保持注释与变量在同一行可能变得不可行,需要更实质性的重写:

1
2
// The blueness of the widget, as a percentage.
var Int: blue_percent = Floor(ComputeBluenessRatio() * 100);

不支持尾随和行内注释的决定是试验性的,如果我们发现在完整的语言设计的背景下需要这样的注释,应该重新考虑。

1.3.2 多行文本注释

不提供多行文本注释的支持。相反,这样的注释是通过在每行前加上相同的// 注释标记来表示的。

要求每行重复注释标记将提高可读性,通过消除非本地状态的来源,并消除了不必要的和不有帮助的注释语法的变化。这种注释的风格在其他语言中很常见,并且得到了编辑器的很好支持。即使在使用/* ... */来注释掉人类可读的文本块的C和C++代码中,也通常在

每行前加上*或其他标记,以使注释更容易阅读。

1.3.3 块注释

我们考虑了各种不同的块注释选项。我们的主要目标是允许注释掉大量的Carbon代码,这些代码可能是或可能不是格式良好的(包括包含块注释的代码,这意味着这样的注释需要嵌套)。考虑的替代方案包括:

  • 完全基于行的块注释,它会删除行,而不考虑它们是否嵌套在字符串字面量中,具有新颖的特性,允许注释掉块字符串字面量的部分内容。这种替代方案的缺点是它会在包含Carbon代码的字符串字面量内部产生令人惊讶的行为。

  • 完全词法化的块注释,其中开头和结尾注释标记之间的令牌序列会被生成并丢弃,词法规则稍微放宽,以避免拒绝格式不良的代码。这将类似于C和C++的#if 0#endif。这种替代方案的缺点是它无法处理不完整的代码片段,如未终止的块字符串字面量。与非词法化语法相比,处理起来也会有些低效,但考虑到块注释预期是短暂的,这可能在很大程度上是不相关的。

  • 一种混合方法,使用//\{//\}作为定界符,这些定界符在非原始字符串字面量中是无效的,并且只对原始字符串字面量有缩进要求。这种替代方案的缺点是它在词法规则中引入了额外的复杂性,通过不同地对待不同种类的字符串字面量。

  • 使用/**/作为注释标记。这种替代方案的缺点是它使用与C和C++相似的语法,但语义有所不同,从而增加了混淆的风险。

然而,考虑到这种注释的有限用例和我们希望最小化我们的创新性,我们在这个提议中没有追求这些选项。

1.3.4 文档注释

我们可以为文档注释添加一个不同的注释语法,也许将文档注释视为产生真实令牌,而不是由词法分析器剥离。但是,在讨论中,有很大的支持使用不像注释的语法来表示文档。例如,我们可以引入一个属性语法,如使用@ <expression>作为声明的前缀来附加属性。然后,可以将字符串字面量属性视为文档:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@"Get the size of the thing."
fn GetThingSize() -> Int;

@"""
Rate the quality of the widget.

Returns a quality factor between 0.0 and 1.0.
"""
fn RateQuality(
  @"The widget to rate."
  Widget: w,
  @"A widget quality database."
  QualityDB: db) -> Float;

这个用例将由未来的提议来探索。

1.3.5 代码折叠注释

一些代码编辑器能够“折叠”源文件的区域,以便于导航。在某些情况下,这些折叠区域可以通过使用注释行来定制。例如,在VS Code中,这是通过包含#region#endregion的注释来实现的:

1
2
3
4
// #region 函数 F 和 G
fn f() { ... }
fn g() { ... }
// #endregion

支持这样的标记作为行注释中的正常文本不需要额外的努力。然而,我们可以考虑引入一个特定的Carbon语法用于区域注释,以鼓励在代码编辑器之间使用通用的表示。这个提议没有涵盖这样的支持,但可以通过一种新形式的注释来处理。

1.4 语言设计

carbon只提供一种注释,它以//开始并运行到行的末尾。在同一行上的注释之前不允许有代码,且引入注释的//后必须跟随空白。

我们选择了这种注释语法,因为它简单、易于理解、易于实现、易于编辑、易于格式化,并且与其他语言的注释语法相似。