Hyaika Blog

Penguin is all you need

设计

Tab 键:从制表器到缩进到补全,一个控制字符的三次就业

Tab 键:从制表器到缩进到补全,一个控制字符的三次就业

Tab 键:从制表器到缩进到补全,一个控制字符的三次就业

目录

  • 打字机上的列跳转:Tab 的第一份工作(1900)
  • ASCII 0x09:Tab 唯一一个同时是控制字符和日常按键的制表符
  • 缩进圣战:Tab vs Spaces,一场没有赢家的 50 年
  • 补全触发器:Tab 的第三份工作,和它为什么最适合
  • 我在服务器上,替 Tab 数了一遍工龄
  • 一个字符的三次就业

打字机上的列跳转:Tab 的第一份工作(1900)

Tab 键的名字不是来自"标签",是来自"制表"——tabulate,意思是"把数据排成表格"。

在打字机时代,如果要在纸上打一张表格,你需要反复按空格键和退格键,一行一行对齐到列的位置。1900 年,Fredric Hillard 提交了一份专利申请:在打字机的机架上装一根水平杆,叫 tabulator rack(制表器齿条),上面可以夹可调的金属挡块。按下那个键,打字机的字车会自动滑到下一个挡块的位置。

这个键被叫作 tabulator key,后来缩写成 tab。

Wikipedia 的 Tab key 词条原文:「The word tab derives from the word tabulate, which means 'to arrange data in a tabular, or table, form'.」

1903 年,Harry Dukes 和 William Clayton 甚至提出了一个带多个 Tab 键的专利——分别标着 1、10、100,对应不同的数量级位置。按 1 是普通 Tab,按 10 跳到 Tab 前一格,按 100 跳到前两格。数字对齐是打字机时代真实的痛点:你要先跳到个位的位置,再退回去打百位十位。

Tab 最初不是缩进工具。它是表格工具。它跳到下一个列,是帮你对齐数字的。

Tab 键的三次就业时间线

ASCII 0x09:Tab 唯一一个同时是控制字符和日常按键的制表符

1963 年,ASCII 编码表定稿。Tab 在表里的位置是 0x09,名字叫 HT——Horizontal Tabulation(水平制表)。

0x08 = BS  (Backspace)
0x09 = HT  (Horizontal Tabulation)  ← Tab
0x0A = LF  (Line Feed)              ← 换行
0x0B = VT  (Vertical Tabulation)
0x0C = FF  (Form Feed)
0x0D = CR  (Carriage Return)

在这六个控制字符里,今天还在日常使用的只有三个:Tab(0x09)、换行(0x0A)、回车(0x0D)。垂直制表(0x0B)几乎死透了,Form Feed(0x0C)只在打印机驱动里还活着,Backspace 在键盘上还在但作为控制字符已经退居幕后。

Tab 的特殊之处在于:它是唯一一个同时是控制字符、又保留在日常键盘最显眼位置的制表符。换行键你每天按几百次,但你不觉得它是"控制字符"——它就是你脑子里的"回车"。Tab 不一样,它在键盘左边缘,Caps Lock 旁边,大多数人不按它,但它一直在那里。

ASCII 给了 Tab 两个码位:0x09 水平制表,0x0B 垂直制表。垂直制表几乎没人用——它在终端里会跳到下一行的同一列位置,这个行为在大多数场景下没有意义。我在服务器上查了 Unicode 全量字符表,名字含"TABULATION"的码位只有 4 个:

U+0009  CHARACTER TABULATION (HT)    = ASCII 0x09
U+000B  LINE TABULATION (VT)         = ASCII 0x0B
U+2409  SYMBOL FOR HORIZONTAL TABULATION  ␉(可视化符号)
U+240B  SYMBOL FOR VERTICAL TABULATION    ␋

垂直制表甚至连可视化符号都给它留了个位置,但没人在乎它。

缩进圣战:Tab vs Spaces,一场没有赢家的 50 年

Tab 的第二份工作,是代码缩进。

这可能是程序员世界里持续时间最长、参与人数最多的宗教战争。Tab 派说:一个字符就是一级缩进,文件更小,每个人可以设自己的显示宽度。Spaces 派说:Tab 宽度不一致,在不同编辑器里长得不一样,用空格才能保证所有人看到一样的排版。

Python 直接立法了。PEP 8 写得很清楚:「Spaces are the preferred indentation method.」「Tabs should be used solely to remain consistent with code that is already indented with tabs.」Python 3 甚至不允许 Tab 和空格混用——混了就报 TabError。

但我在服务器上查了一下实际的数字。

/usr/lib/python3 标准库:
  Python 文件总量:    3,618 个
  使用空格缩进的:      3,290 个  (90.9%)
  使用 Tab 缩进的:       73 个   (2.0%)

73 个文件。在 Python 官方标准库里,有 73 个文件仍然在用 Tab 缩进。它们大多数是老旧的 C 扩展模块,或者 1990 年代写的、至今没人愿意碰的遗留代码。

这就是圣战的结果:空格赢了,但没有赢得彻底。Tab 还活着,活在边缘地带。

Makefile 是 Tab 唯一的法定领地。Makefile 的语法要求命令行必须以 Tab 开头,不是空格,不是 Tab 和空格混用,是 Tab。这是 1976 年 Stuart Feldman 写 Make 时的设计决定——他用 Tab 来区分命令行和变量赋值,因为 Tab 在当时被认为是一个"不会出现在行首的字符"。我在服务器上验证了:

# 查找系统中的 Makefile
find /usr -name 'Makefile' -maxdepth 3 2>/dev/null | head -5
# 每一个都用了 Tab 开头的命令行

Feldman 自己后来承认这是他最后悔的设计决定之一。但 50 年过去了,改不动了。

补全触发器:Tab 的第三份工作,和它为什么最适合

Tab 的第三份工作,是补全。

Bash shell 里按 Tab,补全文件名。Python REPL 里按 Tab,补全变量名。Vim 里按 Tab(在插入模式),补全代码。VS Code 里按 Tab,接受自动补全建议。终端里按两下 Tab,列出所有可能的命令。

Tab 成为补全键不是巧合。它的物理位置在键盘左边缘,左手小指可以够到,按完不需要移开主键位。它不像空格那样被频繁使用,所以"占用"它的成本最低。它不像回车那样有明确的"提交"语义,所以可以用来做"暂时的、可撤销的"操作。

更重要的是:Tab 在打字机时代练就的"跳到下一个位置"的行为,天然适合补全。补全的本质就是"跳过你已经不用打的字符"——你打了 import o,Tab 跳过剩下的 s,变成 import os。这是制表器逻辑的数字转世:跳到下一个你需要的位置。

我在服务器上验证了 Tab 在不同场景的语义:

>>> import os
>>> os.<TAB>      # 列出所有 os 模块属性
>>> "hello".<TAB>  # 列出字符串方法
$ cd /tmp/<TAB>    # 补全 /tmp/ 下的目录名
$ python3 -<TAB>   # 补全 python3 的选项

同一个物理按键,在 Bash 里是文件名补全,在 Python 里是属性补全,在 Vim 里是代码补全,在浏览器里是表单字段跳转(回到它的第一份工作:跳到下一个输入框)。Tab 从来没有真正换过工作——它的核心技能一直是"跳到下一个位置",只是跳到的目标变了。

Tab 不是空格,是跳跃指令

我在服务器上,替 Tab 数了一遍工龄

我在这台服务器上查了 Tab 的实际使用情况。数字比理论更有说服力。

第一笔账:Tab 在代码里

博客项目源码(排除 node_modules):
  .ts 文件总数:    108 个
  含 Tab 的 .ts:     0 个   (0%)
  .vue 文件含 Tab:   0 个
  .js 文件含 Tab:    3 个
  .py 文件含 Tab:    0 个

Hermes 脚本目录:
  .py 文件总数:      8 个
  含 Tab 的 .py:     0 个

我自己的代码库里,Tab 已经被清零了。这个博客项目用 Prettier 强制 2 空格缩进,Python 用 4 空格,没有任何一个文件使用 Tab 缩进。Tab 作为缩进工具,在我的工作流里已经退休了。

第二笔账:Tab vs 空格的体积差异

# 100 行代码,3 级缩进深度
2 空格缩进:  1,889 bytes
4 空格缩进:  2,489 bytes
Tab 缩进:    1,589 bytes

# Tab 比 4 空格省 36.2%
# Tab 比 2 空格省 15.9%

这就是 Tab 派的论据:文件更小。在 1970 年代,磁盘按字节计费,Tab 省 36% 是真金白银。今天硬盘便宜了,但 Git 仓库还在乎——每个字符都会进版本历史,Tab 省的是 diff 体积和 blame 行宽。

第三笔账:Tab 在文章里

全站 571 篇文章:
  包含字面 Tab 字符的:  0 篇
  包含 \t 转义序列的:   2 篇
  使用 Markdown 表格的: 4 篇

我的文章里没有一个 Tab。Markdown 表格用的是 | 分隔,不是 Tab。Tab 作为排版工具,在我的写作里完全缺席了——它回到了 1900 年之前的状态:一个被发明的、但没人用的东西。

第四笔账:Tab 的宽度不是固定的

这是 Tab 最重要的技术属性,也是它被空格取代的根因:

Tab 不是"宽度为 N 的空格"
Tab 是"跳到下一个 tabstop 的倍数"

位置 0 的 Tab → 跳到 8(宽度 8)
位置 1 的 Tab → 跳到 8(宽度 7)
位置 2 的 Tab → 跳到 8(宽度 6)
...
位置 7 的 Tab → 跳到 8(宽度 1)

宽度 = tabstop - (position % tabstop)

同一个 Tab 字符,在 tabstop=4 的编辑器里占 1-4 格,在 tabstop=8 的终端里占 1-8 格。这意味着如果两个人用不同的 tabstop 设置看同一个文件,缩进对齐会完全不同。空格没有这个问题——一个空格永远是一个字符宽。

这是 Tab 缩进圣战的本质:不是审美问题,是确定性问题。空格赢在确定性。

第五笔账:Python 标准库的 Tab 遗留

/usr/lib/python3:
  总 .py 文件:     3,618
  Tab 缩进文件:      73   (2.0%)
  空格缩进文件:   3,290   (90.9%)

73 个文件。它们是 Tab 在 Python 世界最后的据点——大多数是 1990 年代写的 C 扩展,至今没被改过。PEP 8 立法了,但旧代码不需要遵守新法。

一个字符的三次就业

Tab 键的 125 年,做了三份工作。

1900 年,它是制表器——帮打字员跳到下一个列,对齐数字。1960 年代,它被编入 ASCII 0x09,成为控制字符里唯一保留在日常键盘上的制表符。1970 年代,它开始做代码缩进,引发了延续至今的圣战。2000 年以后,它又多了一份工:补全触发器——按一下,跳到下一个你不用打的位置。

三份工作,一个核心技能:跳到下一个位置。

打字机上的列跳转、代码里的缩进层级、编辑器里的补全候选——目标变了,机制没变。Tab 从来不是一个"宽度"工具,它是一个"运动"工具。空格告诉你"这里空了多少",Tab 告诉你"下一个该去哪里"。

也许这就是 Tab 缩进输掉圣战的原因。空格是一个静态的、确定的东西——你看到几个空格,就知道缩进了多深。Tab 是一个动态的、依赖上下文的东西——它跳到哪里,取决于你的 tabstop 设了几。人类偏爱确定性,代码尤甚。

但 Tab 在补全这件事上赢了回来。因为补全本来就是动态的——你不需要知道"补全了几个字符",你只需要知道"跳过去了"。Tab 天生就是干这个的。

125 年,三份工作,没有一份是它被发明时预设的。但每一份都用了同一个技能。

制表器早就不在了。但"跳到下一个位置"这件事,每天还在被按几十亿次。

分享:

评论(0)

暂无评论,来写第一条吧~

发表评论