ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LaTeX内联圈码编号:实现可引用的‘因为所以’逻辑链

LaTeX内联圈码编号:实现可引用的‘因为所以’逻辑链

1. 这个“圈1”“圈一”到底在解决什么问题?

刚接触 LaTeX 公式编号时,我遇到的第一个真实困惑不是“怎么编号”,而是“为什么编号要长成这样”。某天在整理一份数学建模报告,导师批注里写着:“推导过程中的‘因为所以’链,每个逻辑步骤都得有独立可引用的编号,但不能用阿拉伯数字1、2、3——要圈起来,还要区分阿拉伯圈码和中文圈码”。我当时一头雾水:明明\tag{1}就能出 (1),为什么非得是❶?明明\tag{一}就能出(一),为什么有人坚持要❶❷❸而有人非要①②③?更奇怪的是,有些文档里“因为”后面跟❶,“所以”后面却跟②,中间还夹着一个带星号的❸*——这根本不是排版,这是密码学。

后来我才明白,这不是 LaTeX 的 bug,而是学术写作中对逻辑粒度与引用精度的刚性需求。在严谨的数学、物理、工程推导中,“因为 A 所以 B”本身就是一个最小可验证单元;而“因为 A₁ 且 A₂ 所以 B”则需要把 A₁、A₂ 分别编号,供后续交叉引用。这时候,普通公式编号\eqref{eq1}只能指向整行,但读者真正想回溯的是“因为”后面的那条前提——它可能只是半行内容,甚至只是括号里的一个子式。于是,“圈1”“圈一”这类内联式、轻量级、语义化编号就成为刚需:它不打断行流,不占用公式环境,又能被\label和\ref精确捕获。

关键词里没写,但实际场景中高频出现的三个隐性需求是:

  • 视觉降噪:避免整行公式编号(如 (1))与正文段落产生强烈对比,干扰阅读节奏;
  • 层级隔离:主公式用(1)(2),推导步骤用❶❷❸,引理引用用①②③,三者在 PDF 中一眼可辨;
  • 跨语言兼容:中文论文要求“因为❶,所以②”,英文论文则倾向“since (1), therefore (2)”,但底层编号机制必须统一可切换。

我试过直接用\textcircled{1},结果发现它在不同字体下大小不一、上下偏移严重,和 Times New Roman 正文混排时像挂了半颗纽扣;也试过\tikzcircle{1}自定义 TikZ 圆圈,结果编译慢了 3 倍,且在 Beamer 演示中缩放失真。真正稳住的方案,是回归 LaTeX 的底层字符映射逻辑——不是“画个圈”,而是“调用一个已预定义的圈形 Unicode 字符,并确保其在所有字体中几何中心对齐”。

提示:❶(U+2776)和①(U+2460)是两个完全不同的 Unicode 字符,前者属于 Dingbats 区块,后者属于 Enclosed Alphanumerics 区块。LaTeX 默认不支持直接输入❶,但可通过fontspec或unicode-math显式加载支持该区块的字体(如 Noto Sans Symbols2),这才是稳定输出的基础。

2. 为什么\textcircled是个“温柔的陷阱”?

几乎所有初学者都会先撞上\textcircled{1}这个命令。它看起来完美:输入简单、无需额外宏包、PDF 里确实有个带圈的 1。但我在给某高校《高等数学实验》课件做排版时,连续踩了三个坑,才彻底放弃它。

第一个坑是基线错位。\textcircled本质是把数字当作文本盒子,再套一个圆形边框。但数字1的字形高度(ascender)远小于8或0,而\textcircled的圆圈直径是按0的宽度计算的。结果就是:

  • \textcircled{1}的圆圈底部压在基线上,顶部悬空;
  • \textcircled{8}的圆圈上下留白均匀;
  • 同一行里混用\textcircled{1}和\textcircled{8},就像地板上摆着高低不平的玻璃弹珠。

我用\fbox包裹测试过:

\fbox{\textcircled{1}} \fbox{\textcircled{8}}

结果左侧边框紧贴1,右侧大片空白;右侧边框则左右对称。这说明\textcircled并未做字形中心校准,只是粗暴缩放。

第二个坑是字体依赖致命。当文档使用mathptmx(Times Roman 数学字体)时,\textcircled{1}调用的是文本字体的1,但若正文字体是Noto Serif CJK(中日韩字体),而数学字体是Latin Modern Math,\textcircled{1}就会从 CJK 字体取1,导致数字笔画粗细与周围公式严重不协调。更糟的是,某些 CJK 字体(如思源宋体)根本不提供❶类符号,\textcircled{1}会 fallback 到系统默认字体,PDF 里突然冒出一个不搭调的黑体1。

第三个坑最隐蔽:无法参与交叉引用。\textcircled{1}是纯视觉对象,LaTeX 完全不知道它是个“编号”。你不能写\label{step1}在它后面,然后\ref{step1}引用——\ref会返回空值。这意味着所有“因为❶所以②”的逻辑链,只能手敲,无法自动生成、无法全局修改、无法检查引用一致性。某次我手动改了 17 处❶为❷,漏掉一处,答辩时被问“为什么这里说因为❷,但前文没定义❷?”,当场卡壳。

注意:网上流传的“用\raisebox{-0.2ex}{\textcircled{1}}微调”方案,本质是暴力位移,但-0.2ex对1有效,对Ⅰ(罗马数字)就失效;且在不同字号(\small/\footnotesize)下需重新调试,不可复用。

真正可靠的替代方案,是绕过\textcircled,直接调用 Unicode 圈形字符,并用\newcommand封装为可引用的逻辑单元。例如:

\usepackage{unicode-math} \setmathfont{Latin Modern Math} \newcommand{\stepnum}[1]{\text{\char"277\ifcase#1\or 6\or 7\or 8\or 9\or A\or B\or C\or D\or E\or F\fi}}

这段代码用十六进制\char"2776直接调用❶(U+2776),并支持\stepnum{1}到\stepnum{10}。它不依赖字体自动映射,不触发 fallback,且因是纯文本字符,可安全包裹在\label前后。

3. “因为所以”链的 LaTeX 实现:从手动硬编码到自动编号系统

“因为所以”不是语法糖,而是一套微型逻辑标记语言。它的核心诉求是:同一段落内,多个编号必须顺序递增、可独立引用、视觉权重低于主公式、且支持中英文混排。我见过最野路子的实现是手敲:

因为 $A = B$ \textcircled{1},所以 $C = D$ \textcircled{2},又因 $E = F$ \textcircled{3},故 $G = H$ \textcircled{4}。

这在 3 行以内可行,但一旦推导超过 10 步,维护成本爆炸——新增一步要重排所有数字,删减一步要手动改后续所有\textcircled{5}为\textcircled{4}。

真正的解法,是构建一个轻量级计数器 + Unicode 字符映射 + 自动标签系统。关键不在“怎么画圈”,而在“怎么让圈和逻辑步骤绑定”。

3.1 基础版:\newcounter+\stepcounter+ Unicode 映射

LaTeX 原生计数器足够可靠。我们定义一个becausestep计数器,并绑定到 Dingbats 区块:

\newcounter{becausestep} \renewcommand{\thebecausestep}{\ifcase\value{becausestep}\or ❶\or ❷\or ❸\or ❹\or ❺\or ❻\or ❼\or ❽\or ❾\or ❿\fi}

注意:\thebecausestep不是生成数字,而是生成 Unicode 字符。这里用\ifcase硬编码前 10 个,因为 Dingbats 的❶到❿恰好是 U+2776 到 U+277F,连续排列。

使用时:

因为 $A = B$ \stepcounter{becausestep}\label{step:ab} \thebecausestep, 所以 $C = D$ \stepcounter{becausestep}\label{step:cd} \thebecausestep。

效果:因为 $A = B$ ❶,所以 $C = D$ ❷。
引用时:\ref{step:ab}返回❶,\ref{step:cd}返回❷——这才是真正的交叉引用。

但问题来了:\thebecausestep每次调用都返回当前值,而\stepcounter必须显式调用。新手常忘记\stepcounter,导致所有编号都是❶。于是升级为自动版。

3.2 进阶版:\becausestep{}命令封装

把计数和显示合并为一个命令:

\newcommand{\becausestep}[1]{% \stepcounter{becausestep}% \label{#1}% \thebecausestep% }

用法:

因为 $A = B$ \becausestep{step:ab},所以 $C = D$ \becausestep{step:cd}。

简洁!但仍有缺陷:becausestep计数器是全局的,如果文档有多个章节都用“因为所以”,编号会一路累加到❿后崩溃。解决方案是按章节重置:

\makeatletter \@addtoreset{becausestep}{section} \makeatother

这样每\section{}开始,becausestep自动归零。

3.3 生产级版:支持中英文、多级、带星号变体

实际项目中,常需:

  • 中文文档用❶❷❸,英文文档用①②③;
  • 主推导用❶,补充说明用❶*;
  • 同一页面内存在“因为 A 所以 B”和“因为 C 所以 D”两条独立链,需隔离计数。

我最终采用的方案是定义三组计数器:

% 主推导链(全局唯一) \newcounter{mainstep} \renewcommand{\themainstep}{\ifcase\value{mainstep}\or ❶\or ❷\or ❸\or ❹\or ❺\or ❻\or ❼\or ❽\or ❾\or ❿\fi} % 中文补充链(按节重置) \newcounter{cnsubstep}[section] \renewcommand{\thecnsubstep}{\ifcase\value{cnsubstep}\or ①\or ②\or ③\or ④\or ⑤\or ⑥\or ⑦\or ⑧\or ⑨\or ⑩\fi} % 英文链(按小节重置) \newcounter{ensubstep}[subsection] \renewcommand{\thenensubstep}{\ifcase\value{ensubstep}\or (i)\or (ii)\or (iii)\or (iv)\or (v)\fi}

再封装命令:

\newcommand{\mainstep}[1]{\stepcounter{mainstep}\label{#1}\themainstep} \newcommand{\cnsubstep}[1]{\stepcounter{cnsubstep}\label{#1}\thecnsubstep} \newcommand{\ensubstep}[1]{\stepcounter{ensubstep}\label{#1}\thenensubstep} \newcommand{\mainstepstar}[1]{\stepcounter{mainstep}\label{#1}\themainstep\textsuperscript{*}}

实测效果:

  • 中文论文:因为 $A=B$ \mainstep{a},所以 $C=D$ \cnsubstep{b}。→ 因为 $A=B$ ❶,所以 $C=D$ ①。
  • 英文论文:Since $A=B$ \ensubstep{a}, therefore $C=D$ \ensubstep{b}.→ Since $A=B$ (i), therefore $C=D$ (ii).
  • 关键补充:另由引理\ref{lem:xyz},得 $E=F$ \mainstepstar{c}。→ 另由引理 3.2,得 $E=F$ ❷*。

经验:Unicode 圈形字符在 PDF 中的渲染稳定性,取决于unicode-math是否启用及字体是否完整覆盖。若用lualatex编译,务必在导言区加入\usepackage{unicode-math}\setmathfont{STIX Two Math}(STIX Two Math 完整支持 U+2776-U+277F),比Latin Modern Math更可靠。

4.\tag{}的隐藏能力:不只是“换个编号”,而是接管整个编号生态

很多人以为\tag{}就是给单个公式换名字,比如\tag{★}或\tag{L1}。但它的真正威力,在于脱离equation环境的约束,嵌入任意数学模式,并与\label/\ref深度耦合。这正是实现“因为所以”链的技术支点。

4.1\tag{}的底层机制:它本质是一个\hbox+\mathord

LaTeX 中,\tag{}不是简单地在公式右边加文字。它通过\tagform@宏定义外观,底层是:

\def\tagform@#1{\maketag@@@{(\ignorespaces#1\unskip\@@italiccorr)}}

即:把#1(你的输入)放入括号,再用\maketag@@@渲染。而\maketag@@@默认是\hbox{$\m@th\normalfont#1$}—— 一个水平盒子,里面是直立字体的数学模式。

这意味着:

  • \tag{❶}会被渲染为(❶),括号是自动加的;
  • \tag*{❶}(星号版本)则跳过\tagform@,直接输出❶,无括号;
  • \tag*{\textcircled{1}}会调用\textcircled,但如前所述,有基线问题。

所以,因为 $A=B$ \tag*{❶}是合法的,且❶会精准对齐公式基线,因为它是\tag*的直接内容,不受\textcircled的盒子干扰。

4.2 用\tag*{}构建“伪公式环境”

既然\tag*{}能在任意$...$内生效,我们就可以伪造一个“无公式的公式编号”:

因为 $A = B$\tag*{❶}\label{step:a},所以 $C = D$\tag*{❷}\label{step:b}。

效果与\mainstep{}相同,但更轻量——无需定义计数器。缺点是:无法自动递增,每次都要手输❶❷。

于是,把\tag*{}和计数器结合:

\newcommand{\tagstep}[1]{\tag*{\stepcounter{mainstep}\themainstep}\label{#1}}

用法:

因为 $A = B$\tagstep{a},所以 $C = D$\tagstep{b}。

编译后,$A = B$右侧出现❶,$C = D$右侧出现❷,且\ref{a}返回❶。这比\mainstep{}更紧凑,因为\tag*{}本身就在数学模式内,无需退出$。

4.3\tag{}与\nonumber的协同:隐藏主编号,只留逻辑编号

复杂场景中,一个公式既需要主编号(如(3.2)),又需要内部逻辑编号(如❶)。例如:

\begin{align} A &= B + C \nonumber \\ &= D + E \tag*{❶} \label{step:de} \\ &= F \tag{3.2} \end{align}

这里\nonumber抑制第一行编号,\tag*{❶}为第二行添加逻辑编号,\tag{3.2}为整组align指定主编号。PDF 中显示为:

A = B + C = D + E ❶ = F (3.2)

\ref{step:de}返回❶,\eqref{eq:main}返回(3.2),二者完全解耦。

实操心得:\tag{}的内容不参与公式宽度计算,所以❶不会撑开公式行。但若用\tag{①}(U+2460),某些字体中①比❶宽,可能导致右对齐公式溢出页边。建议统一用 Dingbats 区块(U+2776-U+277F),其字符宽度经 ISO 标准化,各字体间差异极小。

5. 全流程避坑指南:从编译报错到 PDF 渲染失真

即使代码逻辑正确,LaTeX 的“玄学”仍会让你在最后一步翻车。以下是我在 50+ 个项目中总结的硬核避坑清单,按发生概率排序:

5.1 编译引擎选择:lualatex是唯一可靠选项

  • pdflatex:不支持直接输入 Unicode 字符(如❶),必须用\char"2776,且fontenc设置稍有不慎就乱码;
  • xelatex:支持 Unicode,但unicode-math与部分字体(如Cambria Math)存在符号映射冲突,❶可能显示为方块;
  • lualatex:原生支持 UTF-8,unicode-math驱动最成熟,STIX Two Math字体对 Dingbats 区块支持最全。

强制配置(导言区):

\usepackage{unicode-math} \setmainfont{Noto Serif} \setsansfont{Noto Sans} \setmonofont{Noto Sans Mono} \setmathfont{STIX Two Math}

Noto系列字体免费、开源、覆盖 Unicode 全字符集,STIX Two Math是目前对数学符号支持最完备的开源字体。

5.2 字体回退陷阱:当❶显示为方块 □

现象:PDF 中本该是❶的地方,显示为白色方块或黑色方块。
根因:当前字体(如Latin Modern Roman)未定义 U+2776 字符,LaTeX fallback 到系统默认字体,而该字体缺失此字符。

三步定位法:

  1. 用\showthe\font查看当前数学字体;
  2. 用\XeTeXglyphindex "❶"(XeLaTeX)或\directlua{texio.write_nl("U+" .. string.format("%X", utf8.codepoint("❶")))}(LuaLaTeX)确认字符码;
  3. 用 Font Book(macOS)或 Character Map(Windows)打开STIX Two Math,搜索 U+2776,确认存在。

终极修复:强制指定❶的字体来源:

\newfontfamily\dingbatsfont{STIX Two Math} \newcommand{\dingnum}[1]{\dingbatsfont\char"277\ifcase#1\or 6\or 7\or 8\or 9\or A\or B\or C\or D\or E\or F\fi}

\dingnum{1}永远调用STIX Two Math的❶,无视上下文字体。

5.3 引用丢失:\ref{}返回 ?? 或空值

常见原因:

  • \label{}写在\tag*{}之后,而非之前。正确顺序:\tag*{❶}\label{step:a};
  • 使用了\nonumber但未配对\tag{},LaTeX 认为该行无编号,\label失效;
  • 计数器未\refstepcounter,\label无法关联到数值。

调试命令:在.aux文件中搜索step:a,应看到类似:

\newlabel{step:a}{{❶}{3}{Because-Solution Chain}{becausestep.1}{}}

若{{❶}{3}...缺失,说明\label未被捕获。

5.4 PDF 渲染失真:圈形字符边缘锯齿或模糊

现象:放大 PDF 到 400%,❶的圆圈边缘有明显像素化。
根因:PDF 渲染器将 Unicode 字符当作位图处理,而非矢量路径。

解决方案:用TikZ绘制矢量圆圈(仅当必须极致清晰时):

\usepackage{tikz} \newcommand{\vectag}[1]{% \tikz[baseline=(char.base)]{ \node[shape=circle,draw,inner sep=1pt] (char) {#1}; }% }

\vectag{1}生成矢量圆圈,但代价是编译速度下降 40%,且1是文本字体,需手动\mathit{1}适配数学模式。权衡之下,我只在印刷级文档中启用此方案。

5.5 中文混排断行:因为❶所以②被错误折行

LaTeX 默认将❶视为普通字符,允许在❶后断行,导致因为❶拆成两行。

修复:用\mbox{}锁定:

\newcommand{\lockstep}[1]{\mbox{\tag*{#1}}}

\lockstep{❶}使❶成为不可分的原子盒子,断行只发生在空格处。

最后分享一个真实教训:某次我用\textcircled{1}生成课件,导出 PDF 后在投影仪上显示正常,但学生用手机 PDF 阅读器打开,❶全部变成1(无圈)。查证发现,该阅读器禁用了嵌入字体,fallback 到系统字体,而安卓系统字体普遍缺失 Dingbats。自此,我所有交付物强制嵌入STIX Two Math字体(lualatex+--output-format=pdf默认嵌入),并用pdfinfo验证字体列表。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进