> For the complete documentation index, see [llms.txt](https://overleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md).

# \expandafter 是如何工作的：展开的含义

[第1部分](/latex/zh-cn/shen-ru-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md) [第2部分](/latex/zh-cn/shen-ru-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md) [第3部分](/latex/zh-cn/shen-ru-wen-zhang/21-how-does-expandafter-work-tex-uses-temporary-token-lists.md) [第4部分](/latex/zh-cn/shen-ru-wen-zhang/20-how-does-expandafter-work-from-basic-principles-to-exploring-tex-s-source-code.md) [第5部分](/latex/zh-cn/shen-ru-wen-zhang/17-how-does-expandafter-work-a-detailed-macro-case-study.md) [第6部分](/latex/zh-cn/shen-ru-wen-zhang/18-how-does-expandafter-work-a-detailed-study-of-consecutive-expandafter-commands.md)

## 引介 TeX 的“展开”概念

在回顾了 [TeX 记号和记号列表](https://www.overleaf.com/learn/latex/Articles/\expandafter_TeX_tokens?preview=true)，接下来我们要讨论的背景主题是 `\\expandafter` 是 TeX 的 *展开*：TeX 的核心过程 `\\expandafter` 旨在处理。

### 关于解释展开的挑战……

对我们大多数人来说，TeX 的展开概念以及其背后的原因/机制可能令人困惑——它们并不容易 *完全地* 以简单、简洁的方式解释。TeX 的许多内部操作/过程之间以相当复杂的方式相互关联，这使得在试图“剖析” TeX 操作的某些方面时会遇到挑战：试图将它们与整个程序分开来讨论。展开无疑属于这一类，因为它是 TeX 行为的一个 *基础* TeX 行为的一个组成部分，并深深嵌入在 TeX 的内部过程之中。不过，本文试图构建对展开的解释，为读者提供一个可据以进一步建立理解的基础。

为了解释 TeX 的展开机制，我们将循序渐进，考察一系列相关/有关主题，并将它们拼接起来，以形成对 TeX 行为这一关键要素的理解。在本文后面，对于更勇于深入细节的读者，我们还会一窥 TeX 本身，总结驱动展开过程的关键源代码。

### 关于 TeX 记号的要点

在下面的讨论中，重要的是要记住，TeX 可以从两个来源获取下一个输入项：

1. **物理文本文件**：生成 *新记号* 通过读取/扫描这些文件中的字符和命令，或者
2. **记号列表**：通过读取一个现有的 *预先准备好的*记号列表，从 TeX 的内存中存储的记号获取输入。

还要记住，TeX 使用记号作为一种方便的方式，把它读入的任何项目（字符或命令）的信息整齐地“打包”起来。

当 TeX 正在生成 *新记号* 时，它会， *在生成记号时*，能够立即访问它正在计算其记号值的项目的信息。例如，在生成新的命令记号时，TeX 首先计算一个称为 `curcs` 的值，以便 TeX 查找它即将生成其记号的命令的信息。

然而，如果 TeX 正在从记号列表中读取（即 *存储的记号*），它必须先对该列表中的任何记号进行“解包”，才能确定每个记号代表什么。例如，TeX 可能正在读取作为宏定义一部分而存储的记号：这些记号本来是由早已读入、转换为记号并存储起来的文本生成的。记号类型（字符或命令）决定了 TeX 的“解包”究竟意味着什么：

* 用于 *命令记号* （记号值 > 4095）：通过计算 `curcs` 经由 $$\text{curcs}=\text{token value}-4095$$ 并使用 `curcs` 来查找有关该命令的信息；
* 用于 *字符记号* （记号值 < 4095）：将字符记号拆分为其组成的配对（字符码，类别码）。

### 一开始……

为了开始探讨展开，我们先看看 TeX 引擎如何对其能够处理的命令集合进行分类或归类。暂且假设“可展开”只是 TeX 命令的一种尚未解释的“属性”。

### “可展开”命令的概念

所有基于 TeX 的排版程序（TeX“引擎”）都理解两大主要“类别”的命令：

* 内置于可执行引擎本身的命令：所谓的 *原始命令*;
* 由用户定义的命令：所谓的 *宏*.

TeX 引擎也使用“可展开性”这一命令“行为”的类型，作为将所有命令划分为两组的机制：

* 可展开命令；
* 不可展开命令。

因此，我们可以认为所有命令（原语加宏）的集合可以如下图所示进行分类：

![显示所有 TeX 命令分类的图示](/files/f7184bd53dcbb45800261acbe3370710a562aa67)

虽然我们还不知道展开/可展开究竟是什么意思，但可以看出：

* 所有宏都被归类为可展开命令；
* 某些原语（内置）命令也被归类为可展开命令（绝大多数不是）。

**关于活动字符的说明**：除了原语和宏之外，还有第三类“可展开”项目：活动字符——任何被分配为类别码 13 的字符也被视为“可展开”，因为它们被当作“微型宏”来处理。

#### TeX 如何知道一个命令是否“可展开”？

从我们对 [TeX 记号的讨论中](https://www.overleaf.com/learn/latex/Articles/\expandafter_TeX_tokens?preview=true) 我们知道，记号不过是 TeX 计算并用来“打包”某个输入项信息的整数：一个字符或一个命令。我们还注意到，给定任何记号（整数）值，TeX 在需要时可以反转记号化过程，对记号进行“解包”，并揭示其所代表的命令或字符的数据。

利用 TeX 创建新记号时产生的数据，或从现有记号中提取的数据——例如存储在宏或其他记号列表中的记号——TeX 可以检查其内部数据存储表，找到该记号所代表的命令（或字符）的详细信息。下图概述了创建命令记号（“打包”）以及当 TeX 需要某些信息时反向进行该过程（“解包”记号）以获得某个特定记号值所代表命令的数据。

![显示 TeX 如何打包/解包记号的图示](/files/175a92cafb0018214c408d726c63a6c1c2bfd385)

### 命令码

TeX 为每个命令（原语或宏）存储的信息中，有一部分叫做其 *命令码*：TeX 引擎（内部）用来对命令进行分类的整数值——具有相似行为/功能的命令共享同一个命令码。

**注意**：对于单个 *非活动*字符来说，其命令码是其 *类别码* 在它被 TeX 读入（扫描）时所分配的值。还要注意，除字符外，命令码完全是 TeX 内部的：不能通过宏或原语命令访问——要查看这些细节，你需要阅读 TeX 的源代码！

下面是一些命令码示例：

* `\hbox`, `\vbox`, `\vtop`, `\vcenter`, `\box` （以及其他）都被归类为“造盒”命令，并共享命令码 20；
* `\def`, `\edef`, `\gdef`, `\xdef` 都属于“宏定义”命令，并共享命令码 97；
* 所有宏都被归入命令码 111、112、113 或 114 之一：不同的命令码取决于定义宏时是否使用了 `\long` 或 `\outer` 在定义宏时：
* 111：非 long、非 outer 宏的命令码（例如， `\def\foo{...}`)
* 112：long、非 outer 宏的命令码（例如， `\long\def\foo{...}`)
* 113：非 long、outer 宏的命令码（例如， `\outer\def\foo{...}`)
* 114：long、outer 宏的命令码（例如， `\long\outer\def\foo{...}`)

TeX 也使用命令码来识别被归类为“可展开”的命令子集。为此，TeX 将可展开命令与超过阈值 100 的命令码值关联起来（Knuth 原始 TeX、e-TeX 和 pdfTeX 所使用的值）。因此：

* *不可展开* 命令（绝大多数）具有命令码 <= 100
* *可展开* 命令具有命令码 > 100

因此，如果 TeX 查找某个特定命令的细节并检测到命令码 > 100，TeX 便立即知道它是可展开的。所有宏以及少数原语（内置命令）都被分配了表明其可展开的命令码：但 TeX 为什么要费心做这个“可展开”分类呢？正如你可能猜到的，这只是意味着这些命令有些“特殊”；也就是说，它们可以接受 TeX 的……过程 *展开*.

**注意（字符）**：除非某个字符被定义为活动字符（类别码 13），否则所有字符都不可展开。活动字符可以看作单字母“微型宏”。

## 是，是，但展开到底是什么？？？

我们注意到，大于 4095 的记号值用于表示命令；因此，TeX 在需要时可以对该记号进行“解包”，以访问该记号所代表的 TeX 命令的信息。TeX 可以查找该命令的命令码值，以确定它是否属于“可展开”的特殊类别。但 TeX 会如何利用这些信息，而“可展开”，也就是展开， *究竟真正意味着什么*?

### 展开：一组操作的统称

在 TeX 处理过程的某些时刻，它需要知道某个记号是否代表一个可展开命令：

* **否**：如果该命令记号 *不会* 属于可展开类别，TeX 就会简单地将其“传递”到下一阶段处理。
* **是**：如果该命令记号 *会* 属于可展开类别，你可以把它看作 TeX 需要采取某种行动，通过执行 TeX 称之为“展开该记号”的过程，或者说执行展开，从而将该特定记号“过滤掉”。

从本质上说，展开涉及 TeX *移除* 该记号从其输入中，并 *替换* 它，替换为由该命令或共享相同命令码的一组命令所特有的“展开过程”产生的记号。

要知道 TeX 何时在进行记号展开、何时没有进行记号展开，需要练习和经验——《The TeXbook》第 215 页列出了可展开记号不会被展开的情况。现实是，TeX 执行展开的过程复杂而细腻。

#### 哪些命令是可展开的？

如果你查看 TeX 引擎归类为可展开的原语（内置）命令列表（例如《The TeXbook》第 212–215 页），就会发现实际上，展开是 TeX 用来描述一组颇为杂糅的命令行为的一个相当宽泛的术语。某个特定（可展开）命令展开的结果确实各不相同，我们可以通过列出一组可展开命令——按其功能分组——来看到这一点。这里并不重要的是单个命令的精确语义，最值得关注的是它们动作的一般后果——它们的展开实际上 *做了什么或实现了什么*.

| **命令**                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | **说明**                                                                  | **展开行为**                                                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <ul><li>所有宏</li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                                      | 存储的命令/字符记号序列                                                            | <p>实际上，宏展开意味着插入一串供 TeX 读取的记号。这包括：</p><ul><li>为任何宏参数创建记号列表——该过程包括识别/吸收分隔记号；</li><li>取出一组存储的记号（宏定义），并准备将第一步创建的参数记号插入（到宏定义中）。</li></ul><p>实际上，宏展开 <em>移除宏记号</em> 从输入中移除，并 <em>并用它</em> 替换为定义中包含的记号。</p> |
| <ul><li><code>\expandafter</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                | 改变展开顺序                                                                  | 记号过滤/重排                                                                                                                                                                                              |
| <ul><li><code>\noexpand</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                   | 阻止展开（是的，令人惊讶的是它被归类为可展开命令）                                               | 延迟展开（某种记号过滤）                                                                                                                                                                                         |
| <ul><li><code>\input</code>, <code>\endinput</code>, <code>\scantokens</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                                    | 与通过源文件输入新的记号来源有关——是的， `\scantokens` 由于它使用所谓的“伪文件”，因此属于这一类               | 充当新记号的来源                                                                                                                                                                                             |
| <ul><li><code>\if</code>, <code>\ifcat</code>, <code>\ifnum</code>, <code>\ifdim</code>, <code>\ifodd</code>, <code>\ifvmode</code>, <code>\ifhmode</code>, <code>\ifmmode</code>, <code>\ifinner</code>, <code>\ifvoid</code>, <code>\ifhbox</code>, <code>\ifvbox</code>, <code>\ifx</code>, <code>\ifeof</code>, <code>\iftrue</code>, <code>\iffalse</code>, <code>\ifcase</code>, <code>\ifdefined</code>, <code>\ifcsname</code>, <code>\iffontchar</code></li></ul> | 开始条件表达式                                                                 | 开始记号“过滤”：决定处理后续哪些记号。                                                                                                                                                                                 |
| <ul><li><code>这段文字将使用俄语。这是为了演示西里尔字母在生成的文档中能够正确显示。</code>, <code>\or</code>, <code>\else</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                   | 终止条件表达式                                                                 | 停止记号“过滤”：决定处理哪些记号                                                                                                                                                                                    |
| <ul><li><code>\csname</code>…… <code>\endcsname</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                                                           | 构造/生成一个命令记号                                                             | 将一系列记号转换为单个命令记号                                                                                                                                                                                      |
| <ul><li><code>\number</code>, <code>\romannumeral</code>, <code>\string</code>, <code>\meaning</code>, <code>\fontname</code>, <code>\jobname</code>, <code>\eTeXrevision</code></li></ul>                                                                                                                                                                                                                                                                                 | 将某物转换为一串记号                                                              | 生成一些记号（表示内部数量或值）                                                                                                                                                                                     |
| <ul><li><code>\the</code>, <code>\unexpanded</code>, <code>\detokenize</code></li></ul>                                                                                                                                                                                                                                                                                                                                                                                    | `\the` 有多种用途，包括插入记号； `\detokenize` 将记号列表转换为字符记号列表； `\unexpanded` 阻止记号展开 | 插入/生成一些记号或控制展开                                                                                                                                                                                       |
| <ul><li><code>\topmark</code>, <code>\firstmark</code>, <code>\botmark</code>, <code>\splitfirstmark</code>, <code>\splitbotmark</code>, <code>\topmarks</code>, <code>\firstmarks</code>, <code>\botmarks</code>, <code>\splitfirstmarks</code>, <code>\splitbotmarks</code></li></ul>                                                                                                                                                                                    | 用于访问具有特殊用途的记号列表的命令                                                      | 访问存储记号（记号列表）的来源                                                                                                                                                                                      |

基于上面的列表，是否可以从这组命令中提取出任何“一般行为”——以进一步细化我们对展开的理解？粗略地说，展开一个命令的结果可归为几个关键活动：

* 记号生成
* 记号插入
* 记号过滤
* 改变/控制下一个记号的展开

### 最后：展开的含义是什么？

我们可以认为，一个命令记号（包括活动字符）的展开涉及 TeX **移除** 从输入中移除该命令记号，并 **替换** 用由其展开行为产生的其他记号替换该记号：生成/插入记号、过滤记号，或改变/控制下一个记号的展开。一旦某个记号被展开，TeX 会继续读取/处理可能由展开过程产生的任何记号。TeX 具体在何时或何处执行展开则是另一个完全不同的问题：由于 TeX 处理过程的细微差别和复杂性，本文无法完全讨论这一点，但我们可以概述 TeX 的主要输入处理行为。

## 展开与 TeX 的结构

可以说，要看出 TeX 如何以及在何处分辨可展开命令和不可展开命令，最好的办法就是查看 TeX 的源代码。

从内部来看，TeX 是一款极其复杂的软件；因此，试图对 TeX 运行中固有的诸多细微行为作出完整描述是不切实际的。如前所述，TeX 在读取记号时究竟何时或何处真正执行展开是“相当复杂”的；因此，下面的讨论假定 TeX 处于需要进行展开的状态。TeX 会读取/创建记号但不执行展开的情况包括为 `\def` 宏定义命令创建/存储记号，或者将记号存储在一个 `\\toks` 寄存器中。

下图对 TeX 结构中处理输入和记号处理的核心部分作了简化概览：其目的在于突出可展开和不可展开记号在 TeX 输入处理过程中被“过滤”的方式。图中使用 C 编程语言来描述各个关键函数，但即使你不熟悉 C 语言，希望基本结构也应当是清楚的。

![](/files/f73e68ad0256aac6752745318ed2339f631f3fdf)

### TeX 结构图说明

概括来说，TeX 的处理由一个名为 `maincontrol()` 的函数驱动，在 TeX 的源代码中，Knuth 将其称为“总指挥”，并如此描述：

> “现在我们来到 `maincontrol` 例程，它包含一个总开关，使 TeX 的各个部分按正确顺序做各自的事情……这是程序的高潮……我们现在处在这个网络的枢纽，处在触及大多数其他部分并将它们连接起来的中枢神经系统。”

下面的描述略有简化，但能够让人感受到基于 TeX 的软件底层结构的“味道”：可展开命令如何在 TeX 的输入处理中被识别（“拦截”）并过滤掉。

本质上，在 TeX 最早的输入处理阶段，它使用当前命令（记号）的命令码值来检测可展开命令（宏、可展开原语和活动字符）；如果识别出某个命令，这类命令就会被“拦截”并“预处理”——即通过展开来处理。通过这一过滤（展开）过程，TeX 会从输入中移除可展开命令，并用由其各自展开行为产生的记号替换它们。展开过程完成后，TeX 会读取由该展开产生的任何记号。只有不可展开项会被传递到 TeX 处理的下一阶段：在“总开关”中，TeX 会执行所有不可展开原语命令并排版字符。

### 理解 maincontrol()

该 `maincontrol()` 函数包含一个大型的所谓 [switch 语句](https://en.wikipedia.org/wiki/Switch_statement)——Knuth 称之为“总开关”——TeX 用它来执行所有 *不可展开* 原语命令：即命令码 <=100 的命令。

为了执行某个特定命令（或字符），这个“总开关”语句会结合 TeX 当前模式：

* 内部/外部竖直模式，或
* 限制/段落水平模式，或
* 行内/显示数学模式

*加上* 每个原语或字符的命令码值——一个 *非活动* 字符的命令码就是其 *类别码* 值。顺便说一下，TeX 的 6 种模式都分配了一个整数值，TeX 可以在切换排版各种构造——数学、段落、盒子等等——时用它来保存和恢复当前模式。下面是 TeX 各种模式所分配的整数：

|          |                    |         |
| -------- | ------------------ | ------- |
| **模式**   | **含义**             | **模式值** |
| 外部竖直     | 段落之间（构建页面）         | 1       |
| 内部竖直     | 紧接在一个 `\vbox{...}` | -1      |
| 水平       | 在排版/构建段落时          | 102     |
| 受限水平     | 紧接在一个 `\hbox{...}` | -102    |
| 显示数学     | 排版 *显示* 方程/公式      | 203     |
| 行内（公式）数学 | 排版 *行内* 方程/公式      | -203    |

请注意，各种模式的补集使用了负值，例如内部与外部竖直模式。 在处理 `maincontrol()`时，TeX 使用当前模式的绝对值——即忽略负号。因此，例如，如果 TeX 处于任一水平模式并需要处理一个类别码为 11 的字符，它会使用 $$102 + 11 = 113$$ 在其“总开关”中的和来分支到处理这些字符在任一水平模式下的代码（构建段落或位于一个 `\hbox`).

#### 获取下一个记号

该 `maincontrol()` 函数调用 `getxtoken()` 以获取下一个输入记号；而后， `getxtoken()` 调用最低层的输入函数 `getnext()`。 `getnext()` 完成其工作并设置关键 *全局变量*, `getxtoken()` 测试刚读入的项是否表示一个宏或一个可展开原语。

关于……的一些说明 `getnext()` 和 `getxtoken()`:

* **`getnext()`**：这是 TeX 的核心输入函数。它读取并处理来自物理文件的字符输入，或者从记号列表中读取记号。请注意， `getnext()` 实际上并不计算记号值，但它会设置若干关键 *全局* 变量（例如命令码和 `curcs`），这些变量会在后续处理过程中使用。
* **`getxtoken()`**：“get expanded token” 使用 `getnext()` 来读取下一个输入项。然后它测试刚读入项的命令码，以检查该项是否表示一个可展开命令：如果是，则展开它或调用该宏。对于不可展开项， `getxtoken()` 计算一个记号值，并将这些细节传递到下一阶段处理：在“总开关”中执行不可展开原语并排版字符。

本质上，正是通过 `getxtoken()` ，所有可展开项在进入 `maincontrol()`之前都会被“过滤掉”：这使得 TeX 的“总开关”只处理不可展开的原语命令。

我们再次强调，上图只是简化示意，因为 TeX 处理的其他环节也会导致记号被展开；例如：

* 当 TeX 查找（扫描）数值时，它会在搜索整数的过程中触发展开（通过调用 `getxtoken()`) ;
* 原语命令 `\romannumeral` 会导致展开——参见文章 [使用……进行展开 `\romannumeral`](https://www.texdev.net/2011/07/05/expansion-using-romannumeral/) 以了解这一事实的细致用法；
* 该 `\edef` （“展开定义”）宏定义命令会强制完全展开；
* TeX 在处理一个……的导言时，会在某些点展开记号 `\halign` 或 `\valign` （这两个命令都与 TeX 的表格排版算法有关）。

[第1部分](/latex/zh-cn/shen-ru-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md) [第2部分](/latex/zh-cn/shen-ru-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md) [第3部分](/latex/zh-cn/shen-ru-wen-zhang/21-how-does-expandafter-work-tex-uses-temporary-token-lists.md) [第4部分](/latex/zh-cn/shen-ru-wen-zhang/20-how-does-expandafter-work-from-basic-principles-to-exploring-tex-s-source-code.md) [第5部分](/latex/zh-cn/shen-ru-wen-zhang/17-how-does-expandafter-work-a-detailed-macro-case-study.md) [第6部分](/latex/zh-cn/shen-ru-wen-zhang/18-how-does-expandafter-work-a-detailed-study-of-consecutive-expandafter-commands.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
