Featured image of post 制表符的前世今生:从打字机到代码缩进

制表符的前世今生:从打字机到代码缩进

一个小小的 Tab,曾经负责让打字机跳到表格列位,后来进入 ASCII、终端、编辑器、Python 和 Makefile,变成程序员又爱又恨的空白字符。

键盘左上角有一个很不起眼的键:Tab

它不像 Enter 那样有仪式感,也不像 Ctrl / Cmd 那样经常参与快捷键组合。很多时候,我们只是用它缩进代码、跳到下一个输入框,或者在终端里补全命令。

但这个小小的键,其实有一段很长的历史。它从打字机时代的「制表」需求出发,进入电传打字机和 ASCII 编码,又在终端、编辑器、编程语言和代码风格争论里活到了今天。

今天就来聊聊:制表符的前世今生

1. Tab 最开始不是为了写代码

Tab 的全称一般被理解为 Tabulator key,也就是「制表键」。

它最早的用途和现代编程没有关系,而是服务于打字机时代的排版:当人们要在纸上打表格、账单、清单时,需要让光标或者打字位置快速跳到某些固定列位。

如果没有 Tab,每一行都要手动敲很多空格:

1
2
3
4
Name        Age     City
Alice       18      Tokyo
Bob         21      Shanghai
Charlie     9      London

在机械打字机上,这是一件很麻烦的事。你需要反复按空格,数错一个位置,整行就会歪。

于是「Tab」出现了。它的核心思想不是「插入几个空格」,而是:

跳到下一个预设好的列位。

这也是为什么它叫「制表符」而不是「四空格键」。它本质上服务的是表格、对齐和定位。

2. 从机械装置到控制字符

在打字机和电传打字机时代,Tab 更像一个机械动作:让打印头或纸架移动到下一个 tab stop,也就是预设制表位。

后来计算机开始使用字符编码表示文字和控制动作,Tab 就从一个物理动作变成了一个控制字符。

在 ASCII 里,水平制表符是:

1
2
3
4
5
6
名称:Horizontal Tab
缩写:HT
十进制:9
十六进制:0x09
转义写法:\t
Unicode:U+0009

也就是说,当你在很多语言里写下:

1
console.log("hello\tworld");

中间那个 \t 并不是两个普通字符 \t,而是一个真正的 Tab 控制字符。

它告诉显示设备或者文本处理程序:这里要进行一次「水平制表」。至于到底显示成多宽,则取决于环境。

3. Tab 到底等于几个空格?

这是很多争论的起点。

从历史上看,Tab 并不等于固定数量的空格。它的意思是「移动到下一个制表位」。如果制表位每 8 列一个,那么它看起来可能像补足到 8 的倍数;如果编辑器设置为 4 列,它看起来就像 4 个空格宽。

举个例子,假设 tab stop 是每 8 列一次:

1
2
3
列号:  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
文本:  A \t B
显示:  A               B

如果 A 在第 1 列,Tab 会把位置推进到下一个制表位附近。它不是简单地「插入 8 个空格」。

用 Python 可以更直观地看:

1
2
3
4
samples = ["A\tB", "AB\tC", "ABCD\tE"]

for s in samples:
    print(s.expandtabs(8))

输出在视觉上大概是:

1
2
3
A       B
AB      C
ABCD    E

同样一个 \t,前面的字符越多,它补出来的可见空白就越少,因为它的目标是「对齐到下一个制表位」。

这也是 Tab 的一个重要特点:

Tab 是语义上的跳格,不是固定宽度的空格。

4. 为什么很多终端默认 Tab 宽度是 8?

很多传统终端和工具里,Tab 默认按 8 列显示。

这和早期终端、电传打字机、Unix 工具链的传统有关。许多老系统默认每 8 个字符设置一个制表位,所以 \t 在终端、cat 输出、less 浏览、C 代码对齐里经常表现为 8 列宽。

这就带来了一个非常经典的问题:你在编辑器里看到的代码,和别人在终端或另一个编辑器里看到的代码,可能并不一样。

比如你在编辑器里把 Tab 显示成 4 列:

1
2
3
if (ready) {
   printf("ok\n");
}

但另一个环境按 8 列显示,它可能就变成:

1
2
3
if (ready) {
       printf("ok\n");
}

这里我用 表示一个真实的 Tab。你能看出来,Tab 的显示宽度不是文本本身决定的,而是阅读环境决定的。

5. 程序员为什么为 Tab 吵架?

制表符真正变成「宗教战争」,是在代码缩进里。

经典争论是:

  • 用 Tab 缩进;
  • 用空格缩进;
  • 用 Tab 表示缩进层级,用空格做对齐;
  • 编辑器里按 Tab,但保存时转换成空格。

各派都有自己的理由。

支持 Tab 的人会说:

1
2
3
4
一个缩进层级 = 一个 Tab
每个人可以在自己的编辑器里显示成 2、4、8 列
文件更小
语义更清楚

支持空格的人会说:

1
2
3
4
所见即所得
不会因为编辑器 tabWidth 不同而乱掉
Diff 更稳定
适合精确对齐

问题在于,Tab 和空格一旦混用,就很容易制造事故。

看这段代码:

1
2
3
if user.is_active:
    print("active")
	print("send email")

肉眼看起来,两行 print 可能差不多。但第一行前面是 4 个空格,第二行前面可能是一个 Tab。Python 对缩进非常敏感,所以这类混用可能直接触发:

1
TabError: inconsistent use of tabs and spaces in indentation

Python 不是小题大做。它只是在说:缩进既然影响语义,就不能含糊。

6. Makefile:Tab 的保留节目

如果说 Python 让大家害怕混用 Tab 和空格,那么 Makefile 则让很多新人第一次认识到:

有些地方,Tab 不是风格问题,而是语法要求。

在传统 Makefile 中,命令行前面必须是一个真实的 Tab:

1
2
hello:
	@echo "Hello, Tab"

注意,这里的 \t 表示真实 Tab。在实际文件里,它不是反斜杠加字母 t,而是一个制表符。

如果你写成空格:

1
2
hello:
    @echo "Hello, Spaces"

就可能看到类似这样的错误:

1
Makefile:2: *** missing separator.  Stop.

这个错误信息曾经折磨过无数人。它的意思大概是:Make 期待看到一个分隔命令的 Tab,但你给了几个看起来很像的空格。

这也是制表符历史包袱最明显的地方之一:一个几十年前延续下来的设计,到今天仍然在构建系统里提醒我们,空白字符并不总是「无所谓」。

7. HTML 里的 Tab:会被折叠的空白

到了网页世界,Tab 又换了一种命运。

在普通 HTML 文本流里,连续的空格、换行和 Tab 通常会被折叠成一个空格显示:

1
<p>A			B</p>

浏览器里看到的往往只是:

1
A B

如果你想保留 Tab 和空格,需要用 <pre>white-space 相关 CSS,或者代码块。

比如:

1
2
<pre>A	B
AB	C</pre>

或者:

1
2
3
4
.code {
  white-space: pre;
  tab-size: 4;
}

CSS 里甚至有一个专门的属性:tab-size,用来控制 Tab 显示成多少列宽:

1
2
3
pre {
  tab-size: 4;
}

这就很有意思了。Tab 的本体还是那个 U+0009,但它在不同环境里的样子,一直由显示规则决定。

8. 编辑器里的现代 Tab

现代编辑器通常会把 Tab 做成一个很灵活的东西。

比如 VS Code、JetBrains 系列、Vim、Neovim、Emacs 都可以配置:

1
2
3
4
5
{
  "editor.tabSize": 4,
  "editor.insertSpaces": true,
  "editor.detectIndentation": true
}

这段 VS Code 配置的意思大概是:

  • 按一次 Tab,看起来是 4 列;
  • 实际插入空格而不是真实 Tab;
  • 打开已有文件时,自动检测缩进风格。

很多项目还会用 .editorconfig 统一风格:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true

[*.py]
indent_style = space
indent_size = 4

[Makefile]
indent_style = tab

这其实是一个很现代的答案:不要靠口头约定,也不要靠团队成员猜,直接把空白规则写进项目配置里。

9. Git 里的 Tab:看不见,但能制造噪音

Tab 还是 Git diff 里的常客。

如果一个文件把 Tab 全部转换成空格,可能逻辑上一行都没改,但 diff 会变得非常大:

1
2
3
4
5
6
-if (ready) {
-	return true;
-}
+if (ready) {
+    return true;
+}

这类改动不是不能做,但最好单独提交。否则真正的逻辑修改会被淹没在空白变化里。

Git 也提供了一些查看空白问题的方式:

1
git diff --check

它可以帮助发现行尾空格等问题。你也可以在 review 时忽略空白变化:

1
git diff -w

不过 -w 只是阅读 diff 时的辅助,不代表空白字符真的不重要。尤其是在 Python、YAML、Makefile 这类对空白敏感的文件里,空白就是语法的一部分。

10. 今天我们该怎么对待 Tab?

我的建议很简单:不要把 Tab 当成道德问题,把它当成工程约定。

不同场景可以有不同选择:

场景建议
Python按 PEP 8,通常使用 4 个空格
JavaScript / TypeScript跟随项目格式化工具,如 Prettier
Go使用 gofmt,它会按 Go 的习惯处理缩进
Makefile命令行前必须使用真实 Tab
Markdown 表格通常用空格更稳定
需要可访问缩放的缩进Tab 也有合理性,因为用户可自定义显示宽度

真正重要的是:

  1. 同一个项目保持一致。
  2. 用格式化工具自动处理。
  3. .editorconfig 或语言工具写清楚规则。
  4. 不要在同一段缩进里混用 Tab 和空格。
  5. 知道哪些文件里 Tab 是语法要求。

Tab 不是坏东西,空格也不是正义本身。很多争论的根源,其实不是字符本身,而是团队没有把约定固定下来。

最后

制表符很像计算机世界里一枚小小的化石。

它来自打字机时代的表格排版,经过 ASCII 变成控制字符,在终端里保留了 8 列传统,在 Makefile 里留下语法痕迹,又在现代编辑器里变成可以自动转换、自动检测、自动格式化的缩进工具。

它看不见,却一直在场。

每次我们按下 Tab,可能只是想让代码往右缩进一格。但在这个动作背后,其实藏着一条从机械打字机、电传终端、Unix 工具链,到今天 IDE 和 Git 工作流的长线。

所以,下次再遇到「Tab 还是空格」的问题,也许可以先别急着开战。

先问一句:这个项目的约定是什么?

然后把它写进配置里,让工具去执行。这样 Tab 和空格都能安静下来,代码也能少一点无意义的争吵。

Powered by Hugo & Stack