\begin{tabular}{lcr} 里的 {lcr},是 LaTeX 中少数几个 写程序而不是写文字 的地方。除了列数和对齐方式,你还能在这里指示它在每个单元格前后插入声明、把列间空白换成别的东西,或者成组重复列。而且这套 列指定 的词汇并非出自同一个设计,而是靠捐赠一点点攒起来的:\newcolumntype 原本属于 David Carlisle 的另一个宏包 newarray.sty,array 手册中还记着「David 慷慨同意将其并入」。本页从 l、c、r、p{width} 出发,依次追踪 m{width} 与 b{width}、>{…} 与 <{…}、@{…} 与 !{…}、*{n}{…}、w 与 W,直到能让表格精确达到指定宽度的 tabularx 的 X 列。
哪些列指定需要哪个宏包
纯 LaTeX 只提供 l、c、r、p{width}、|、@{…} 和 *{n}{…}。其余的——m{width}、b{width}、>{…}、<{…}、!{…}、w 与 W,以及 \newcolumntype——全都来自 array 宏包(TeX Live 2024 附带的是 2023 年 10 月 16 日的 v2.5g,作者 Frank Mittelbach)。X 列来自 tabularx(2023 年 7 月 8 日的 v2.11c,作者 David Carlisle),而 tabularx 本身又依赖 array。由于 booktabs 和 tabularx 会在内部载入 array,它常常已经生效,但显式写上 \usepackage{array} 更稳妥。
想知道自己的文档里跑的是哪一套,只需 故意写一个不存在的列指定字母。纯 LaTeX 会回答 ! LaTeX Error: Illegal character in array arg.,而载入 array 后,抱怨则来自宏包:! Package array Error: Illegal pream-token。所以,如果你写了 m{width} 却被告知 “Illegal character”,这句话真正的意思是:你忘了载入 array。
段落列的垂直位置 — p、m、b,以及固定宽度的 w、W
会换行的段落列有三种,差别只在 单元格的垂直位置。照搬 array 说明书的说法:p{width} 等价于 \parbox[t]{width},即 顶部对齐;m{width} 把内容放在 行的上下中央;b{width} 与 \parbox[b] 一致,即 底部对齐。这个差别在同一行既有高单元格(换成多行的 p 列或大图片)又有单行单元格时最明显。默认的 p 会让旁边的短单元格贴在行顶,与高单元格的中部对不上;改用 m{width} 就能自然对齐。
如果希望宽度固定但不换行,array 提供了 w{align}{width} 和 W{align}{width}。两者基本等同于 \makebox[width][align]{cell}:行为像 l、c、r,同时把列宽钉死。二者的区别在于出事的方式——w 在内容超宽时会 默默地压到旁边,而 W 在同样情况下会给出 overfull box 警告。不过要注意,W 溢出时总是向右伸出,所以看上去对齐方式会改变。若需求是“固定宽度但溢出时要提醒我”,那么 W 才是正解。
| 指定符 | 垂直位置 / 宽度 | 换行? | 提供者 |
|---|---|---|---|
p{wd} | 顶部对齐,等价于 \parbox[t]{wd} | 是 | 标准 |
m{wd} | 在行内垂直居中 | 是 | array |
b{wd} | 底部对齐,与 \parbox[b]{wd} 一致 | 是 | array |
w{l}{wd} | 相当于 \makebox[wd][l];过宽时默默压出 | 否 | array |
W{l}{wd} | 与 w 相同但溢出时警告;溢出部分向右 | 否 | array |
X | 宽度经计算的 p 列,用于让表格达到目标宽度 | 是 | tabularx |
使用段落列时有两个小默认值值得记住。第一,在 p、m、b 内部,段落缩进 \parindent 为 0;想恢复的话可以写成 >{\setlength{\parindent}{1em}}p{5cm}。第二,array 还添加了一个名为 \extrarowheight 的长度。给它正值时,只会增加 每行的高度,而不改变深度——这是解决 \hline 看上去贴到大写字母顶部的最直接处方(array 手册本身也写明,为文档中展示的那张表设置了 1pt)。
在每个单元格前后插入 — >{…} 与 <{…}
array 宏包的核心是 >{decl} 与 <{decl}。>{decl} 会在该列 每个单元格内容之前 插入 decl,<{decl} 则在 之后 插入。写 >{\bfseries}l,左对齐列的所有单元格都会变粗体,无需逐格书写。>{\itshape}c 得到居中斜体,>{\color{red}}r 得到右对齐红色。对段落列还可以放对齐声明:>{\raggedright}p{4cm} 会生成「换行但不两端对齐」的列。
不过这里有一个 严重陷阱。\raggedright、\raggedleft、\centering 三者都会在内部 重新定义行末的 \\。因此,如果把它们之一放进 >{…} 的列位于表格 最后一列,\\ 就不再结束行,下一行的 & 溢出,于是报出 ! Extra alignment tab has been changed to \cr.。原因在列指定里,错误却指向正文某一行,所以第一次几乎没人能看出来。解决办法是在声明 末尾 加上 array 的 \arraybackslash:写 >{\raggedright\arraybackslash}p{4cm} 就能恢复行末功能。它对非末列没有害处,所以养成总是写上的习惯更稳妥。
\arraybackslash 的来历颇有趣。它最初是由 tabularx 引入的,正是为了修复与 \raggedright 的这一冲突。如今定义 \def\arraybackslash{\let\\\tabularnewline} 的是 array.sty 本体,而 tabularx 只保留了「若尚未定义就自行定义」的保险。换句话说,这个工具诞生在一个宏包,如今却住在另一个宏包里。
\usepackage{array}
% ...
% bold labels; a ragged-right wrapping column; a whole column in math mode
\begin{tabular}{ >{\bfseries}l >{\raggedright\arraybackslash}p{5cm} >{$}c<{$} }
\hline
Symbol & Meaning & \multicolumn{1}{c}{Value} \\
\hline
c & Speed of light in vacuum, a defining constant. & 2.998\times10^{8} \\
g & Standard gravitational acceleration near the surface. & 9.807 \\
\hline
\end{tabular}第三列使用的 >{$}c<{$},正是 > 与 < 最初被设想的用途。每个单元格前后各加一个 $,于是该列所有单元格都作为 行内数学 排版,数字或符号密集的列不必再逐格写 $…$。反过来,在本身已处于数学模式的 array 环境中这样写,两个 $ 会相互抵消,使 只有该列进入文本模式。不过表头行需要留意:直接写 Value 会被当作公式排版,得到字距松散的斜体。像上面的例子那样写 \multicolumn{1}{c}{Value},就能让这一格回到文本模式。
还要记住一条限制:>{…} 和 <{…} 里只能放 声明(declaration)。像 \bfseries、\itshape 这类后面不取引数的形式没问题,但 不能以需要引数的命令结尾。>{\textbf} 会开始把后续单元格内容当作它的引数读取,并以最尴尬的方式失败:不报错,也没有任何内容变粗。array 手册说得很明白——>{\textbf} “既不会让整列变粗,也不会让第一个字符变粗”。想加粗就用声明形式 \bfseries。
调整列边界 — @{…} 与 !{…}
LaTeX 默认会在两列之间插入 \tabcolsep 的空白——每侧 6pt,合计 12pt。在列指定的该位置放上 @{decl},用 array 手册的说法就是「抑制列间空白,并插入 decl. 代替」:那处空白消失,换成你写的东西。主要有两种用途。第一是 裁掉表格外侧留白:在列指定两端放 @{}(空声明),会移除第一列左侧和最后一列右侧的 \tabcolsep,使表格与正文左边缘齐平——这是 booktabs 表格中常见的写法。第二是 插入分隔符:把数字拆成整数部分列和小数部分列,中间放 @{.},小数点便会固定位置对齐。
与 @{…} 相对的是 !{decl}。它可以写在能放竖线 | 的位置,并用任意 decl 代替竖线。决定性的区别在于 !{…} 不会移除普通的列间空白。array 手册自己的例子很直观:c!{\hspace{1cm}}c 得到的间距是 原有空白再加 1cm,而 c@{\hspace{1cm}}c 得到的是 恰好 1cm。若想在列之间画虚线又保留内边距(比如把 arydshln 的虚线命令放进去),!{…} 就是正确选择。顺带一提,载入 array 后竖线 | 的行为也会改变:列间距会按线条粗细 变宽,而在标准 LaTeX 中线条位于已有空白内,宽度不变。
重复与命名列 — *{n}{…} 与 \newcolumntype
当许多相同指定的列连续出现时,可以用 *{count}{spec} 折叠。按 array 手册的定义,它等价于 spec 的 count 份拷贝,所以六个居中列可写作 *{6}{c} 而非 cccccc。重复单元也可以包含多个指定:*{3}{|c}| 展开为 |c|c|c|。它真正的价值体现在重复含 >{…} 或 @{…} 的长指定时——*{4}{>{$}c<{$}} 一次就造出四个居中的数学模式列。
如果同一种组合在文档各处反复出现,用 \newcolumntype{letter}{…} 给它命名会好读得多。array 手册本身就以 \newcolumntype{C}{>{$}c<{$}} 为例,此后只需写 {C C C}。\newcolumntype 接受与 \newcommand 相同的可选引数,因此也能构造以宽度为参数的列类型。当定义堆积、自己都记不清时,可以写 \showcols:它会把当前所有生效的 \newcolumntype 定义列表输出到日志。
让表格达到目标宽度 — tabularx 的 X 列
至此的列指定,宽度都由 你 来定。反过来,「让整张表恰好等于 \textwidth」的场合,就要用 tabularx 的 X 列。像 \begin{tabularx}{\textwidth}{l X r} 那样先给出 目标宽度,标记为 X 的列就会通过计算来补足差额。X 之所以容易理解,是因为机制极其朴素:正如手册所写,X 指定在算出正确宽度后会被替换为 p{某个值}。在源码中,默认值就是一行 \newcommand{\tabularxcolumn}[1]{p{#1}}。因此 X 不过是「宽度稍后填入的 p 列」,这也正是同样的修饰对它有效的原因——>{\raggedright\arraybackslash}X 的行为完全符合预期。想让内容垂直居中?重新定义即可:\renewcommand{\tabularxcolumn}[1]{m{#1}}。
\usepackage{tabularx}
% ...
% two X columns, the second twice as wide as the first
\begin{tabularx}{\textwidth}{@{}
l
>{\hsize=.667\hsize\linewidth=\hsize}X
>{\hsize=1.333\hsize\linewidth=\hsize}X @{}}
\hline
Key & Short note & Longer note \\
\hline
alpha & fits in the narrow column & twice as wide, so prose wraps less often \\
\hline
\end{tabularx}默认情况下所有 X 列宽度相同,但像上面那样写 >{\hsize=…\hsize\linewidth=\hsize}X 就能 改变权重。不过手册规定了两条戒律:保持 X 列宽度之和不变(上例中 0.667 与 1.333 合计为 2,仍相当于两个标准 X 列),以及 不要使用跨越任何 X 列的 \multicolumn,因为宽度计算并未把它考虑在内。若出现异常,可声明 \tracingtabularx,日志中会打印每一轮尝试的列宽。
还要记住两个与 X 有关的典型症状。如果非 X 列的合计已经超过目标宽度,tabularx 拒绝给 X 列负宽度:它会发出 Package tabularx Warning: X Columns too narrow (table too wide),并退而把它们设为 1em 宽,于是表格反而比你要求的更宽。其次,tabularx 的主体实际上是作为命令的引数被读取的,而且在搜索宽度时 表格会被反复重排,所以其中写 \verb 会引发 LaTeX Warning: \verb may be unreliable inside tabularx。tabularx 与 tabulary、tabularray 的比较,以及根本放不进纸面的表格该怎么办,属于「宽表与自动宽度」一页。
组合多种机制的示例,以及数字对齐的现代答案
最后是一个把上述工具塞进单个列指定的例子。第 1 列是粗体标签列(>{\bfseries}l);第 2 列是左齐且会换行的段落列(>{\raggedright\arraybackslash}p{5cm});第 3、4 列用 *{2}{…} 一起生成居中的数学模式列;两端的 @{} 裁掉外侧留白。能在一行里写下多个机制是列语言的强项,同时也让它难读,所以给常用组合用 \newcolumntype 命名才是务实之举。
\usepackage{array}
% ...
\begin{tabular}{@{} >{\bfseries}l >{\raggedright\arraybackslash}p{5cm} *{2}{c} @{}}
\hline
Symbol & Meaning & Value & Unit \\
\hline
$c$ & Speed of light in vacuum, a defining constant. & $2.998\times10^{8}$ & m/s \\
$g$ & Standard gravity, an approximation near the surface. & $9.807$ & m/s$^2$ \\
\hline
\end{tabular}有一件事,这门列语言 最好别用来做:按小数点对齐数字。请使用 siunitx 宏包的 S 列,而不是前面草草提到的 @{.}。S 列会解析每个数字,正确对齐整数部分、小数点、小数部分、指数乃至不确定度,同时整理数位分组和有效数字格式。顺带一提,array 手册在给出自己的小数点对齐代码后,亲自加了一句「这段代码写得不好」,并指引读者去看 David Carlisle 的 dcolumn.sty 中更好的版本——宏包手册这样坦率地评价自己的示例,实属罕见。