> 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/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md).

# TeX 宏究竟如何工作：第 6 部分

[第 1 部分](/latex/zh-cn/geng-duo-zhu-ti/19-how-tex-macros-actually-work-part-1.md) [第 2 部分](/latex/zh-cn/geng-duo-zhu-ti/20-how-tex-macros-actually-work-part-2.md) [第 3 部分](/latex/zh-cn/geng-duo-zhu-ti/21-how-tex-macros-actually-work-part-3.md) [第 4 部分](/latex/zh-cn/geng-duo-zhu-ti/22-how-tex-macros-actually-work-part-4.md) [第 5 部分](/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.md) [第 6 部分](/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md)

## 导言与概览：目前为止的故事

在本系列前 5 个部分中，我们已经看到了：

* TeX 如何读取输入文件中的字符，并使用类别码来识别不同“类别”的字符，随后将它们转换为字符记号和命令记号；
* 宏实际上由四个部分组成：

```
<TeX 宏原语><宏名><参数文本>{<替换文本>}
```

其中：

* `<TeX 宏原语>` = 以下之一 `\def`, `\edef`, `\gdef` 或 `\xdef`;
* `<宏名>`= 你的宏的名称，例如 `\foo`;
* `<参数文本>` 可以是“空”（不存在），也可以是一串分隔符记号和宏参数记号；
* `<替换文本>` 是宏的实际主体：也就是在调用宏时被“执行”（展开）的那一部分。
* 如何 `<参数文本>` 部分可以包含各种各样的记号，以及 TeX 如何把这一部分当作“记号模板”，用来将一次宏调用与其原始定义进行匹配，并算出与该宏一起使用的参数——以及 TeX 如何期望你对宏的使用方式与其原始定义相匹配；
* 在 TeX 内部，一个宏定义被存储为一串连续的记号，表示 `<参数文本>` 和 `<替换文本>` 各部分。

当你使用一个宏命令时，TeX 会先检查它是否带有参数。如果有，TeX 就必须识别在你的宏调用中实际使用的 *参数* 。TeX 必须将你的宏调用与它存储在内存中的“记号模板”定义进行测试。具体来说，TeX 使用你宏的内部（已存储）定义中的 `<参数文本>` 部分作为模板，通过它找出 *记号* 哪些是实际参数，哪些 *记号* 只是作为分隔符存在。

## 宏展开的含义

现在我们终于可以转到最重要的话题：TeX 如何处理宏参数并真正执行宏：TeX 将这一过程称为 *宏展开*.

### 不过首先，来看一个简短的例子：有点奇怪？

为了“设置场景”，解释 TeX 处理宏及其参数的机制，我们将用一个简短的例子来说明需要考虑的问题。

#### 参数首先被转换为记号

下面这个例子基于 David Salomon 所著的 [The Advanced TeXbook](https://www.amazon.co.uk/Advanced-Texbook-David-Salomon/dp/0387945563) 第 114–115 页中讨论的一个例子。之所以选它，是因为它用一个非常短的 TeX 宏很好地概括了核心思想。

在正常的 TeX/LaTeX 操作中， `$` 符号的类别码是 3（“math shift”），它会将 TeX 切换进/出行内数学模式（`$...$`）或陈列数学模式（`$$...$$`）——当然，LaTeX 使用 `\(..\)` 和 `\[..\]` 来实现相同的用途。

假设我们想要一个宏，把一个 `$` 符号的类别码改成比如 11，这样我们就能像普通字符一样排版它。我们可以使用 TeX 原语命令 `\catcode` ，而我们对此类宏的第一次尝试， `\docat`，可能是

```
\def\docat #1{\catcode`\$=11 #1}
```

然而，当我们尝试这样使用它时，像这样

```
\begin{document}
\def\docat #1{\catcode`\$=11 #1}
I paid \docat{$90} for that book.
\end{document}
```

我们期望 TeX 排版出 `I paid $90 for that book.` 但它却失败了，并给出一条错误信息：

```
! Missing $ inserted.
<inserted text>
                $
<to be read again>
                   \par
l.7
```

从这个错误看起来，我们宏参数中使用的 `$` 似乎仍然在触发 TeX 排版数学模式；显然，TeX *并没有* 改变我们宏参数中使用的 `$` 的类别码（`$90`）。问题是 *为什么* 为什么 TeX 没有把 `$` 的类别码改为 11，并把它当作普通字符来排版呢？简短的答案是，TeX 会先把宏参数转换为记号 **在** 然后再把它们送入 `<替换文本>`的记号列表中——不过我们会更详细地看看其底层机制。

我们需要记住的一点是，我们所说的 TeX 使用文本/字符，只与 TeX 正在读取的文件内容有关：一旦 TeX 读入了任何字符，我们就进入了 *记号*的世界。TeX 宏调用处理的是 *记号*，而不是实际的 *书写/文本表示形式* 的 TeX/LaTeX 命令——随着我们继续这个例子，这一点会变得更清楚。

起初，我们可能会认为，在 `\docat` 中使用这个宏 `I paid \docat{$90} for that book.` 与直接写出等价的 TeX（或 LaTeX）代码是一样的——比如下面这个，它 *确实* 可以工作：

```
\begin{document}
I paid \catcode`\$=11 $90 for that book.
\end{document}
```

![Overleaf 上运行的某段 TeX 代码](/files/418530a862ae283ea08da545eaad4fe8149f9be0)

然而，正如我们上面看到的，TeX 处理宏参数的方式会产生一个结果（`! Missing $ inserted.`），它与直接写出 TeX 代码完全不同：现在我们来探究 *为什么* 这是如何发生的。

### 宏和参数作为记号列表

要完全理解 `\docat` 宏及其参数（`$90`）的行为，以及它为什么会失败，我们再次需要把 `\docat` 宏定义和所用的任何参数（当 \docat 被调用时）看作 *记号列表*，而不是字符序列。

当 TeX 扫描你的输入文本时，它会把 `\docat` 识别为一个宏命令；之后它会检查它是否带有参数——TeX 如何做到这一点，将在下一节中为对细节感兴趣的读者解释。

#### 给喜欢细节的读者……

在宏被调用后，TeX 会检查其存储定义记号列表中的第一个记号是否是 **结束匹配** 记号：如果是，TeX 就可以确定该宏不带任何参数。

**一个例子**

下面的节点列表图比较了两个宏的记号列表：

* `\def\foo A#1B{#1}`：这有 `<参数文本>` 的记号值 `A#1B`，因此 **结束匹配** 记号是 **不** 第一个记号，所以 TeX 会继续查找参数；
* `\def\foo{X}`：这没有 `<参数文本>` 部分，因此 **结束匹配** 记号是记号列表中的第一个，TeX 知道不需要寻找任何参数。

![TeX 如何检查宏是否带参数](/files/6307de6020ab476db1568889c1ae25220cf12600)

## 走向“终极高潮”：展开

让我们回顾一下问题：为什么下面这个宏没有起作用；也就是说，为什么 TeX 不改变 `$` 符号的类别码，这些符号出现在 `\docat` 宏的参数中，例如 `\docat{$90}`?

```
\begin{document}
\def\docat #1{\catcode`\$=11 #1}
I paid \docat{$90} for that book.
\end{document}
```

如上所述，当 TeX 扫描你的输入并识别出一个宏命令——在 TeX 准备执行它的时候——TeX 会先检查该宏是否带有参数。如果有，TeX 还需要进一步扫描输入文件，以识别用户在这个特定宏调用中提供的实际 *参数* TeX 必须这样做， **在** 才能调用实际的宏代码。显然，TeX 需要确定用户想提供给宏的数据。

为了识别输入中的参数（用户的宏调用），TeX 会依据该宏在内部存储的定义：具体来说，是存储的宏定义（记号列表）中的 `<参数文本>` 部分——它提供了一种“记号模板”。借助这个“记号模板”，TeX 必须判断用户宏调用中的哪些 *记号* 只是 *分隔符* （本质上是“标点”），哪些 *记号* 构成了一个 *参数*。当 TeX 遇到存储的宏定义 **匹配参数** 记号时 `<参数文本>` 部分（“记号模板”），它就知道要开始形成一个 *记号列表* ，用于那个特定参数。

一旦 TeX 识别出需要确定用户参数，TeX 就会扫描输入以生成记号，并非常仔细地逐个记号与存储的宏定义进行检查。TeX 会持续收集某个参数的记号，直到它检测到一个实际上是分隔符的记号，或者检测到 **结束匹配** 记号：在这两种情况下，TeX 就知道该停止寻找构成该参数一部分的记号了。

### 为什么 \docat 宏失败了

如前所述， *在* TeX 要真正调用一个宏，它必须先识别并准备好将要与该宏一起使用的任何参数。然而，为了识别参数并准备将其送入宏中，TeX 必须把每个参数生成成一个 *记号列表*：这就是 `\docat`失败的原因。

在我们的例子中，我们给 `\docat` 提供了一个 `$90` 作为参数， *但这个参数首先被* 转换为一个记号列表 *在* ，因为 TeX 在扫描宏调用时会将该参数转换为记号——也就是说，在宏真正被调用之前。这里，对于参数 `$90`，TeX 会生成三个字符记号：每个 `$`, `9` 和 `0`.

下面的图示显示了为参数 `$90`生成的记号列表，在被送入 `\docat` 宏的预期用法：

![为宏参数生成的 TeX 记号列表](/files/c5782babf0b68ff5210bedcb6ee671801beccbda)

在上面的图中，我们可以清楚地看到参数记号列表包含了 `$` 作为一个基于类别码 3 的字符记号。

正如我们在第 1 到第 3 部分看到的，字符记号是使用字符被读入时生效的类别码值创建的 *——也就是说，在参数的记号列表被创建（转换成记号）的时候。当参数正在被分词时，*&#x5B8F;还没有被执行，因此我们在宏调用中放入的类别码更改（ `\docat` \catcode\`\\$=11`）不会`) *不会* 影响用于生成参数记号的类别码。

一旦 TeX 生成了一个表示 `$90`参数的记号列表，那三个字符记号就会被送入实际的宏 `<替换文本>`。然而，这会导致 `$` 被作为一个 *字符记号* 送入，而这个字符是用类别码 3 创建的：“math on”；而我们已经看到，一旦字符记号形成，其附带的类别码就是永久的。 `$` 是 *不* 送入宏 *的不是一个字符*，而是一个 *字符记号* 基于 `$` 具有类别码 3。

### 运行 \docat：宏展开

TeX 把“执行”一个宏的过程称为 *宏展开*；在本文作者看来，这个术语有点令人困惑，但它是约定俗成的，所以我们继续使用它。

#### 宏展开的真正含义

在 TeX 检测到用户输入中的 `\docat` 命令后，它会扫描参数并为其参数生成一个记号列表（`$90`）。要执行（展开）这个宏，TeX 会把注意力从用户的输入文件上移开，并开始读取其中包含的记号 `\docat`的节点列表中的以下内容： `<替换文本>` 记号列表，存储在 TeX 的内存中。

当 TeX 处理 `\docat`的定义时，它随后会看到并执行最初用于定义宏的那串记号（`**catcode**`, ``**`**``, `**\$**`, `**=**`, `**1**`, `**1**`, `**#1**`).

下面的图示展示了展开 `\docat` 宏的过程：TeX 停止从输入文件中获取记号，开始从 `<替换文本>` 部分读取记号，来自 `\docat` 存储在内存中的宏定义。TeX 继续执行这些预先准备好的记号，直到它看到一个 **输出参数** 记号，它会指示 TeX 在此处读取（“注入”）并“执行”参数记号。在我们的例子中，那就是表示 `$90` 的三个字符记号，而这会导致错误，因为 `$` 预先准备好的字符记号的类别码是 3。由于我们处理的是字符记号，而不是字符， `$` 不受前面由 `）不会`.

![展示 \docat 宏展开过程](/files/f82a898577b206a636c7a67c401d8b405e37449e)

在 TeX 处理完表示 `）不会`的记号后，针对 `$` 的类别码更改就会开始生效。随后 TeX 会遇到一个名为 **输出参数** 的“特殊记号”，它告诉 TeX 要 **插入** 该参数的记号列表。然而，该记号列表是三个字符 *记号*，其中第一个是一个 `$` 记号，其类别码被设为 3（“math on”）：宏内部先前的类别码更改无法影响这个字符记号，因此 TeX 把该记号视为开始数学处理的信号，从而导致宏失败。

### \docat 宏能修复吗？

从上面的讨论可以清楚地看出，出现在宏参数中的任何字符，都会使用分词发生时生效的类别码来进行记号化——在我们的例子中，也就是 *总是* 早于 `<替换文本>` 中的参数 `\docat` 宏真正执行之前。那我们怎样才能确保宏的参数的类别码被改变呢？

一种方法是把 `\docat` 修改为一个无参数宏，它只做类别码更改——它没有任何要被分词的参数。然后我们再使用第二个宏， `\getarg`，它接收一个参数，并安排让该宏在 `$` 的适当类别码生效时对其参数进行分词。

```
\begin{document}
\def\docat{\catcode`\$=11 \getarg} % No parameters, calls a second macro \getarg
\def\getarg#1{#1} %1 parameter whose argument will be tokenized
现在你可以这样运行，它就能工作：

I paid \docat{$90} for that book.
\end{document}
```

当我们使用新版的 `\docat` （像这样 `\docat{$90}`）时， *看起来* 好像 `$90` 仍然被用作 `\docat` 宏的参数。然而，如上所述，当 TeX 检测到输入中的 `\docat` 时，它会检查是否有参数：现在没有了，所以 TeX 就继续执行（展开）它。 `\docat` 的展开是记号序列 `**catcode**`, ``**`**``, `**\$**`, `**=**`, `**1**`, `**1**`, `**space**`, `**getarg**` ，而这一切发生在 *在* TeX 开始读取（分词）输入文件中下一个字符——也就是组 `{$90}`。请记住，当 TeX 展开一个宏时，它获取下一个输入的方式，是读取该宏定义记号列表中包含的记号；也就是说，从其存储在内存中的 `<替换文本>` 部分。

TeX 将处理并执行 `\docat` 的展开，并检测到 *记号* **`getarg`**，把它识别为一个表示带参数命令的记号。此时，TeX 会扫描输入文件以寻找 **`getarg`**&#x7684;参数：这些字符是 `{$90}`。与往常一样，它们会被记号化，但由于 TeX 已经读取并处理了 `\docat`的展开，这些字符 `$90` 是在 `$` 的类别码已被改为 11 时被记号化的。对`<替换文本>`的定义（ `\getarg` ）仅仅是 `#1` 这意味着把提供的参数排版出来，而这正是实际发生的结果，最终生成了一个 `$` ，其类别码为 11，并被安全地排版出来。

## 结语：节点中的故事

重写 `\docat` 以使用宏 `\getarg` 所引发的一系列事件，包含在下面带注释的节点列表图中，该图展示了宏 `\docat`的展开过程。希望仔细研究此图的读者可以下载该图为 [PDF](https://assets.ctfassets.net/nrgyaltdicpt/7MOBdavza4WxAEQGISEEht/cabe5097d9063a6415bc553cd38237e6/newdocatexpansion.pdf) 或 [SVG](https://images.ctfassets.net/nrgyaltdicpt/2FIqqVRXjakdAzTpZsV0m9/10ccc1353741d42d3df18f1692d8aa84/newdocatexpansion--plain.svg) 文件以供离线使用。

![展示修改后的 \docat 宏和 \getarg 宏的展开过程](/files/0ba1aab41d92127f73d2bfdf0cfc26c14e948837)

[第 1 部分](/latex/zh-cn/geng-duo-zhu-ti/19-how-tex-macros-actually-work-part-1.md) [第 2 部分](/latex/zh-cn/geng-duo-zhu-ti/20-how-tex-macros-actually-work-part-2.md) [第 3 部分](/latex/zh-cn/geng-duo-zhu-ti/21-how-tex-macros-actually-work-part-3.md) [第 4 部分](/latex/zh-cn/geng-duo-zhu-ti/22-how-tex-macros-actually-work-part-4.md) [第 5 部分](/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.md) [第 6 部分](/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.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/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.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.
