> 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/17-how-does-expandafter-work-a-detailed-macro-case-study.md).

# \expandafter 是如何工作的：一个详细的宏案例研究

&#x20;[第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)&#x20;

## 案例研究：\expandafter 示例，出自《 $$\varepsilon\mathrm{\text{-}{\TeX}}$$ 手册

该 $$\varepsilon\mathrm{\text{-}{\TeX}}$$ 排版引擎源自 Knuth 的 TeX 软件，最初被设想为通往 [新排版系统](https://en.wikipedia.org/wiki/New_Typesetting_System) （NTS），使用 Java 编程语言编写。 $$\varepsilon\mathrm{\text{-}{\TeX}}$$ 最早于 20 世纪 90 年代末开发，旨在增加一套新的原语命令，以提供 Knuth 原始程序中没有的附加功能。尽管 $$\varepsilon\mathrm{\text{-}{\TeX}}$$ 自首次发布以来，已定期获得更新；如今它并未被广泛用作独立的排版引擎，尽管其创新已被后来的 TeX 版本吸收：pdfTeX、XeTeX 和 LuaTeX。

该 [$$\varepsilon\mathrm{\text{-}{\TeX}}$$ manual](http://mirror.ox.ac.uk/sites/ctan.org/systems/doc/etex/etex_man.pdf) 包含一个启发性的宏示例，它巧妙地利用了 `\\expandafter`:

```
    \def\foo#1#2{\number#1
    \ifnum#1<#2,
    \expandafter\foo
    \expandafter{\number\numexpr#1+1\expandafter}%
    \expandafter{\number#2\expandafter}%
    \fi}
```

`\\foo` 实现了一种循环机制，因此 `\foo{7}{13}` 会产生 `7, 8, 9, 10, 11, 12, 13`；然而， `\\foo` 并未使用任何 *对变量的赋值* 来控制循环过程——这使它成为一个值得详细探究的有趣宏。

### 背景说明：表达式与赋值

……的一个重要元素是 `\\foo`的代码在于它使用了命令 `\numexpr`，这是四个相关原语命令之一，最初由 $$\varepsilon\mathrm{\text{-}{\TeX}}$$: `\numexpr`, `\dimexpr`, `\glueexpr` 和 `\muexpr`引入。它们的目的是构造所谓的 *表达式* ，它们允许分别对 TeX 中 number、dimen、glue 或 muglue 类型的值进行计算/操作。正如《 $$\varepsilon\mathrm{\text{-}{\TeX}}$$ 手册》第 8–9 页所讨论的那样， *表达式* 的一个重要特性是其求值（计算）不需要 TeX 执行任何 *赋值*.

从编程角度看，赋值是将某个变量设定（分配）为特定值的过程；例如，将 `\count` 寄存器 `99` 设为包含值 `12345` 经由 `\count99=12345`。在 TeX 处理过程中还会发生许多其他类型的赋值——例如让标记寄存器包含一系列标记、让盒寄存器包含盒内容，等等。

要执行一次赋值，例如 `\count99=12345`，TeX 需要调用（执行）实现以下行为的内部代码： `\count` ，或任何其他执行某种赋值的原语。不过，有些时候 TeX 正在进行纯粹的 *展开* ，而在这些时候，这类赋值不会被执行——*在 TeX 处理的那个时刻*。这种情况的例子包括以下命令：

* `\edef\command {*token list*}` “扩展定义”宏定义命令，它会展开以下内容中的标记： *标记列表* 并将结果存储为 `\command`.
* `\write *number* {*token list*}` 展开以下内容中的标记： `*token list*` 并将它们写入由以下内容表示的文件： `*number*`.
* `\directlua {*token list*}` 这个 LuaTeX 原语命令用于将 Lua 代码传递给内置 Lua 解释器。以下内容中的所有标记都会 `*token list*` 在传递给 Lua 解释器执行之前被完全展开。

#### \edef 的快速示例

如果我们编写如下基本宏：

```
     \def\mycount{\count99=12345}
     \edef\mymacro{\mycount}
```

`\edef` 会展开 `\mycount` 为其组成标记，但不会再进一步：包含在以下内容定义中的任何命令都不会被执行： `\mymacro` 将会被执行：也就是说，以下赋值： `12345` 更改为 `\count99` *此时不会发生*；只有当我们调用 `\mymacro` 时，TeX 在执行处理以下内容的代码时，这个赋值才会发生： `\count` 原语。当 TeX 正在执行 *仅扩展操作* 任何赋值都会在 TeX 处理的后续阶段执行，而不是在扩展过程本身中执行。

#### 为什么这里要关注赋值？

在编写执行循环的代码时——无论使用哪种编程语言——通常都会指定一个变量作为“循环计数器”：用于控制循环执行的次数。循环一般通过测试这个指定的循环计数器变量是否已达到某个特定值来控制——该变量会在每次循环迭代中递增（或递减）。然而，修改循环计数器变量意味着要给它赋予一个新值，而这在 TeX 中通常需要原语命令 `\advance` 来递增（或递减）存储在某个 `\count` 寄存器中的值。正如我们所见，在 TeX 的纯扩展过程中，这类赋值（包括变量递增）无法进行：宏 `\\foo` 巧妙地绕过了这一限制。

### 回到对 \foo 的解释

这个宏 `\\foo` 能够控制循环过程 *没有* 而无需给任何变量赋值：它利用扩展过程中产生的数据来控制循环发生的次数：这些数据值存储在临时标记列表中。借助我们对 TeX 使用（生成）临时标记列表方式的了解，我们可以仔细看看它究竟是如何 `\\foo` 实现其结果的。

**请记住**：我们正在逐步分析一个宏的执行过程，此时其定义的原始文本——包含在一个物理 `.tex` 文件中——已经被扫描（由 TeX 读入）并转换为表示该宏定义的标记列表。简而言之，我们正在跟踪 TeX 对这些存储的 *记号* 在 TeX 内存某处包含的宏定义中读取并处理标记时的过程。宏定义的 TeX 代码中原本存在的任何空格字符（位于 `.tex` 文件）中的空格，要么在 TeX 扫描该文本查找命令时被吸收了（空格作为终止符），要么已被转换为标记，例如逗号后面的空格字符（`,`）在 `\ifnum#1<#2,` ，它源自对行尾字符（`\r`）转换为空格。

由于以下内容中的 TeX 代码： `\\foo` 使用了多个 `\\expandafter` 命令，我们将通过给每个 `\\expandafter`添加下标来辅助说明，以表示我们所指的是哪一个。此外，我们还会扩展由 `\\expandafter` 更改为 $$\mathrm{T^i\_1}$$ 和 $$\mathrm{T^i\_2}$$处理的标记的表示法，表示标记 $$\mathrm{T\_1}$$ 和 $$\mathrm{T\_2}$$ 用于 `\expandafter<sub>i</sub>`: `\expandafter<sub>i</sub>` $$\mathrm{T^i\_1T^i\_2}$$

下面是加注说明的宏代码：

```
    \def\foo#1#2{\number#1
    \ifnum#1<#2,
    \expandafter1\foo
    \expandafter2{\number\numexpr#1+1\expandafter3}%
    \expandafter4{\number#2\expandafter5}%
    \fi}
```

`\\foo` 以 `\number#1` 开头，它使用可展开命令 `\number` 将第一个参数值转换为其排版后的表示。\number 命令的工作方式是生成一个临时标记列表，其中包含表示该数字值中各个数字的字符标记，而该数字值是 `\number` 正在处理的。该标记列表会成为 TeX 的下一个输入源。在这里，该标记列表被读取，其中的标记被输出，用于排版 `\number` 。接着，宏执行测试 `#1`.

下一步， `\ifnum#1<#2` 以下参数是否 `#1` 小于传入给 `#2`。如果是，则输出一个逗号（`,`）标记（排版出来），随后跟着一些空格，这些空格来自逗号后面的换行字符生成的标记（`,`）。这个空格字符最初是在 TeX 从以下内容读取这一行时生成的 `.tex` 文件中。

宏继续处理下一段代码，这一段是其运作的核心：

```
    \expandafter1\foo
    \expandafter2{\number\numexpr#1+1\expandafter3}%
    \expandafter4{\number#2\expandafter5}%
    \fi}
```

本质上，这段代码生成一系列临时标记列表，从而导致对以下内容的多次调用： `\\foo` 宏，并在 if 测试 `\ifnum#1<#2` 不再为真时终止。不过 *如何* 循环是如何被控制的，因为这里并没有发生赋值：那么“循环计数器”在哪里呢？

让我们先看这段代码 `\expandafter<sub>1</sub>\foo\expandafter<sub>2</sub>`。请注意，我们将使用下标表示法 `<sub>标记</sub>` （或者 `<sub>(标记)</sub>`）来提醒自己，在这里，TeX 正在读取/处理数值（整数）标记值。

在这里，以下标记作为 `\expandafter<sub>1</sub>`:

* $$\mathrm{T^1\_1} =$$`\foo<sub>标记</sub>` 作为输入，被读入并存储以供后续 *重新插入* 回到输入中
* $$\mathrm{T^1\_2} =$$`\expandafter<sub>2 (token)</sub>` 并被展开

对于 `\expandafter<sub>2</sub>` 我们有：

* $$\mathrm{T^2\_1} =$$`{<sub>标记</sub>` ，它会被保存以供稍后 *重新插入* 回到输入中
* $$\mathrm{T^2\_2} =$$ `\number<sub>标记</sub>` 并被展开

**注意：**`\number` 是一个可展开命令，其用途是“转换为标记”：也就是说，把一个数值转换为一系列表示该数值的字符标记。当 `\number` 被展开时，TeX 做的第一件事就是扫描输入以寻找整数：这一过程会触发进一步展开。

**故事的关键在于：** 这里， `\number` 作用于 *表达式* `\numexpr#1+1` 它计算 `#1+1`的值。该计算结果随后由 `\number` 处理，将其转换为一个临时标记列表，其中包含表示以下值的字符标记 `#1 + 1`。该临时标记列表由 `\number`生成，最终会被读入并作为对以下内容的另一次调用的第一个参数： `\\foo`。它不是通过 `\advance` 和赋值来递增循环计数器，而是使用 `\numexpr` 创建一个新值，而无需进行赋值。通过这种机制，控制循环的变量（`\\foo`的参数 `#1`）得以递增，循环迭代得以控制并终止：相当巧妙！

接着， `\expandafter<sub>3</sub>` 被处理，得到：

* $$\mathrm{T^3\_1} =$$`}<sub>标记</sub>` ，它会被保存以供稍后 *重新插入* 回到输入中
* $$\mathrm{T^3\_2} =$$`\expandafter<sub>4（标记）</sub>`，它会被展开：

对于 `\expandafter<sub>4</sub>` 我们有：

* $$\mathrm{T^4\_1} =$$`{<sub>标记</sub>` ，它会被保存以供稍后 *重新插入* 回到输入中
* $$\mathrm{T^4\_2} =$$`\number<sub>标记</sub>` 它会被展开并将 `#2` 转换为另一个临时标记列表。

最后，`\expandafter<sub>5</sub>` 被展开：

* $$\mathrm{T^5\_1} =$$`}<sub>标记</sub>` ，它会被保存以供稍后 *重新插入* 回到输入中
* $$\mathrm{T^5\_2} =$$`\fi<sub>标记</sub>`，这是一个可展开命令。

  对以下内容的展开 `这段文字将使用俄语。这是为了演示西里尔字母在生成的文档中能够正确显示。` 实际上终止了 `\ifnum` ，并实际上结束了该宏的这一轮迭代。TeX 现在完成重新插入所有由多次 `\\expandafter` 命令临时保存的标记：这会生成一系列单标记标记列表，这些列表来自每个 `\\expandafter`所保存的标记。此外，TeX 还通过以下内容的作用创建了标记列表： `\number`.

### 组装标记列表

本质上， `\\foo` 宏生成了一系列标记列表：你可以把 `\\foo` 看作一个“标记列表制造工厂”。这些标记列表会被 TeX 读取，成为下一轮输入源。巧妙之处包含在以下内容更早期的某个操作中： `\\foo`:

```
    \expandafter1\foo\expandafter2
```

借此 `\\foo` 设法再次调用自身，但参数不同，这些参数存储在由以下内容构造的标记列表中 `\number`。为了让这些标记列表整体表现得像一次宏调用，大括号 `{` 和 `}` 都已通过以下内容的作用被保存并重新插入到输入中（作为单标记列表）： `\\expandafter` 命令。

![由 \foo 宏生成的标记列表](/files/fb7ce4f9c1f1e20685415c623e3ee663955209c1)

&#x20;[第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/17-how-does-expandafter-work-a-detailed-macro-case-study.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.
