Skip to content

4.2.哈希表

龙腾道 edited this page Dec 13, 2023 · 11 revisions

  首先,根层级本身就是一个哈希表,可以用以 = 分隔的键值对,来表示它的内容(每条独占一行):

a="根表.a"
b="根表.b"

  其中键名可以是符合正则表达式 ^[A-Za-z0-9_-]+$ 的“裸键”,也可以用基本字符串表示不符合这一规则的情况:① ②

"[.] =\t#"="根表['[.] =\t#']"
''="根表['']"

  注意纯数字构成的“裸键”,依然是一个常规的字符串键名(例如 1 = true,等价于 '1' = true"1" = true),它并不表示这是一个数组索引项。同理,1-1 = true 意味着 '1-1' = true,而不是一个数学运算表达式(TOML 中没有运算表达式)。

① TOML 0.4 开始才对键名有此明确的字符限定。在此之前,键名的规则比较模糊,只要求不要有一些语法相关的字符,也没有提及如果需要出现这些字符,该如何表示。

② TOML 0.5 明确表示,也可以使用 TOML 0.3 加入的字面量字符串,来表示键名。


  可以用以 [ ](在英文中最醒目的符号)包围的小节名来表示子表(独占一行):

a="根表.a"
b="根表.b"
[sectionA]
a="根表.sectionA.a"
b="根表.sectionA.b"
[sectionB]
a="根表.sectionB.a"
b="根表.sectionB.b"

  小节名可以是以 . 分隔的多个键名:

a="根表.a"
b="根表.b"
[chapterA.sectionA]
a="根表.chapterA.sectionA.a"
b="根表.chapterA.sectionA.b"
[chapterA.sectionB]
a="根表.chapterA.sectionB.a"
b="根表.chapterA.sectionB.b"

  小节名始终是相对于根表的,因此总是需要书写从根表触发的完整路径。这样,我们就获得了清楚地表示多层级的能力(而 .ini 只能套一层),且无论在文档的什么位置看到一个表头,都不需要联系上下文判断完整的路径;同时避免了对缩进的依赖(像 YAML 或 JSON ③ 那样),而不依赖缩进,就能大大地简化编辑行为,用小节名的冗余,换取无穷无尽的缩进的冗余,特别是在想要严谨地表示多行字符串时,缩进是一个非常尴尬的因素。完整路径中途径的节点(如上例中的 chapterA)会被自动注册,因此没有必要显式地逐一陈列([chapterA])。

  注意:正如“裸键” 1 等价于 '1'1.1 实际上意味着一组“点分隔键” '1'.'1',而不是像浮点数一样作为一个整体被当作 '1.1' 对待。

③ JSON 的语法虽然不要求缩进,但那对于人的阅读是极不友好的。


  对于这样表示很啰嗦的小表,从 TOML 0.4 开始可以用内联哈希表(不允许跨行,除非换行出现在子内联数组或多行字符串值内)表示:

a={x=1,y=1,z=1}
b={x=2,y=2,z=2}
c={x=3,y=3,z=3}

注意:不能使用键值对或小节语法,在外部对内联表进行追加操作;内联表一经定义就不可变动。


  从 TOML 0.5 开始,键值对中的键,也可以像小节名一样用 . 分隔了:

title="title"
meta.charset="utf-8"
form.feedback={tagName="input",attrs.type="url",attrs.placeholder="https://"}

语法之上的附加规则

  小节和点分隔键语法虽然赋予了使用者以更灵活的书写能力,但随意穿梭却可能造成负面效果,导致一个表的内容被分散在各处,使得阅读时无法一目了然地得知一个表下究竟有哪些内容。因此,规范对这种能力进行了一定程度的限制,有一些语法上可以但实际不被允许或不被鼓励的行为。其背后的尺度并不复杂,但是如果不能领会其精神,就很容易感到费解,所以在此逐一说明。

  其中,不被鼓励但实践中可能需要的行为包括“父小节后置”、“相邻表交叉陈列”和“由键和小节共同组成一个表”:

[parent.importantA]
[parent.importantB]
[parent]
others='补充一些相对次要的内容'
[A.meta]
[B.meta]
[A.body]
[B.body]
x.type='...'
y.type='...'
x.data='...'
y.data='...'
table.x=1
table.y=2
[table.z]

  前两种情况被允许,是因为小节是特别醒目的,故而对小节本身的顺序以及小节内局部的键顺序的灵活处理,并不会造成多大的混乱。所以实际上是否使用这种能力,取决于具体的章节语义场景下,这种行为是否具有实际意义支撑,规范对此并不粗暴禁止。当然即便使用,也不是乱用,纵使从机器的角度看,只要是这样写都可以归结为是无序的,但实际上我们应当保证在人类阅读时,是“形散而神不散”的。规范没有强制加以约束,仅仅是因为没有一种外在的尺度可以简单地传达这一准则。

  第三种情况被允许,是因为它本质上相当于:

[table]
x=1
y=2
[table.z]

  只要别故意跳开很远写,其代价比较小;甚至根据具体语义,可读性有所提升也未必不可能。因此规范没有粗暴地禁止,而是交由书写者自己把握。

  明确被禁止的行为包括“多次打开同一个小节”、“通过键访问其它小节”和“将键创建的表作为小节打开”,因为它们几乎没有真实的用例证明其价值,同时代价显然过于高昂:

[A]
x=1
y=2
[B]
[C]
[A]
z=3
[A.B.C]
x=1
[A.B]
C.y=2
[A]
B.C.z=3
A.B.x=1
[A.B]
y=2
[A]
C={x=1,y=2}

  它们只是单纯地导致同一个表下的内容被无意义地分散,而且不进行通览(不仅是表头大纲,而且是内部的所有键的细节),根本无法预判究竟还有多少处。

  规范对于以上两类情况处置策略上的差异,背后其实透露着这样一种意味:“小节”和“键”虽然从最终解析结果的角度看,都是等价的表,但是它们在源码的阅读层面上,是有醒目程度的本质差别的。因此,小节之间的关系,以及小节内作为一个局部、其中键之间的关系,都是可以相对灵活处理的;而导致键分散在多个小节中的行为,或混淆键与小节层次地位的行为,则动摇到了根本。我们可以把一个 TOML 文件,想象成左侧是一个所有小节头组成的目录、右侧是一个每次只能打开浏览其中一个小节的详情的阅读器,这样就能非常自然地理解规范对于各种行为完全不同的自由度背后的统一尺度。