> For the complete documentation index, see [llms.txt](https://ayakaleaf-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://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md).

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

[第 1 部分](/latex/zh-cn/shen-du-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md) [第 2 部分](/latex/zh-cn/shen-du-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md) [第 3 部分](/latex/zh-cn/shen-du-wen-zhang/21-how-does-expandafter-work-tex-uses-temporary-token-lists.md) [第 4 部分](/latex/zh-cn/shen-du-wen-zhang/20-how-does-expandafter-work-from-basic-principles-to-exploring-tex-s-source-code.md) [第 5 部分](/latex/zh-cn/shen-du-wen-zhang/17-how-does-expandafter-work-a-detailed-macro-case-study.md) [第 6 部分](/latex/zh-cn/shen-du-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 获取下一个输入项可以有两个来源：

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 何时执行记号展开、何时不执行，需要一定的练习和经验——《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>\fi</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 把它称为“chief executive”，并这样描述：

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

下面的描述略作简化，但能给出基于 TeX 软件底层结构的一种“味道”：可展开命令是如何在 TeX 的输入处理过程中被识别（“拦截”）并过滤掉的。

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

### 理解 maincontrol()

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

要执行某个特定命令（或字符），这个“big switch”语句会使用 TeX 当前模式和以下信息的组合：

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

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

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

请注意，对于各种模式的互补形式会使用负值，例如内部与外部竖直模式的区别。在处理 `maincontrol()`中的命令时，TeX 使用当前模式的绝对值——也就是说，忽略负号。例如，如果 TeX 处于任一种水平模式，并且需要处理一个类别码为 11 的字符，它会使用和 $$102 + 11 = 113$$ 在其“big switch”中分支到处理这些字符的代码，该代码用于任一种水平模式（构建段落或位于一个 `\hbox`).

#### 获取下一个记号

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

关于 `getnext()` 和 `getxtoken()`:

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

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

我们再次强调，上面的图只是一个简化，因为 TeX 处理的其他区域也会导致记号被展开；例如：

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

[第 1 部分](/latex/zh-cn/shen-du-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md) [第 2 部分](/latex/zh-cn/shen-du-wen-zhang/22-how-does-expandafter-work-the-meaning-of-expansion.md) [第 3 部分](/latex/zh-cn/shen-du-wen-zhang/21-how-does-expandafter-work-tex-uses-temporary-token-lists.md) [第 4 部分](/latex/zh-cn/shen-du-wen-zhang/20-how-does-expandafter-work-from-basic-principles-to-exploring-tex-s-source-code.md) [第 5 部分](/latex/zh-cn/shen-du-wen-zhang/17-how-does-expandafter-work-a-detailed-macro-case-study.md) [第 6 部分](/latex/zh-cn/shen-du-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://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-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.
