> 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/22-how-tex-macros-actually-work-part-4.md).

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

[第 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)

## 引言与概述

在第1–3部分中，我们深入探讨了一些底层背景细节，为理解 TeX 宏的工作方式做准备。在本文中，我们将“出来透透气”，回顾一些 TeX 宏的基本实用原理，为第5和第6部分中的另一次深入探讨做好准备。在整篇文章中，我们将展示使用 TeX 原语命令定义的示例宏 `\def`：我们不会使用那个也许更熟悉的 LaTeX 命令 `\newcommand`。这样做有一个很好的理由：我们的目标是理解 TeX 宏行为背后的基本原理，而为此我们需要使用内置于 *内置于* TeX 软件中。LaTeX 命令，例如 `\newcommand`，它们本身就是宏：具有特定编程行为的命令，而这些命令最终是由更底层的 TeX 原语命令层层构建而成。为了更好地理解 TeX 的基本行为，我们必须使用 TeX 原语，而不是 LaTeX 宏。

### 我们要往哪里去？

本质上，我们正在努力将宏解释为一种特殊形式的记号列表：当你指示 TeX 定义一个宏时，它会创建一个记号序列（记号列表），并将其存储在内存中，关联到你定义的一个名称上。在第5和第6部分中，我们将较为详细地查看宏的记号列表，不过如果你想先跳出本文去阅读一些背景信息，可以在以下内容中找到 [什么是 TeX 记号列表？](/latex/zh-cn/shen-du-wen-zhang/54-what-is-a-tex-token-list.md)

宏及其参数的记号列表包含一些额外的细节，我们将详细展开——并使用大量示例。为了更好地理解 TeX 处理宏的行为，有一个关键因素真的需要记住：TeX 只在输入处理的最早阶段考虑字符：从那一刻起，始终都是记号！在本文以及本系列剩余文章中，我们将探讨记号在 TeX 宏中所扮演的角色。

## 一个宏的四个部分

### 提醒：宏参数与宏实参

在开始之前，值得先提醒自己宏的 *参数* 与宏的 *参数*实参，因为本文中我们会一直使用这两个术语。假设你定义了一个宏 \foo

```
\def\foo#1#2{This is #1, this is #2}
```

这些构造（记号） `#1`, `#2` （直到 `#9`）被称为宏的 *参数*：可以把它们看作你调用宏时将使用的实际数据的“占位符”：

```
\foo{alpha}{beta}
```

这里， `alpha` 和 `beta` 是 *参数* 用于此次对 \foo 的调用：宏的实参是输入到参数占位符中的实际值（`#1`, `#2`... `#9`）在定义宏时使用的。

### 宏定义：包含4个元素

我们先从一个看起来也许有些正式的宏定义说起，但它确实为后续讨论提供了一个有用的框架。

任何宏定义都由4个部分组成：

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

其中：

* `<TeX 宏原语>`：其中之一是 `\def`, `\edef`, `\gdef` 或 `\xdef`;
* `<宏名>`：赋予你的宏命令的名称，例如 `\foo`;
* `<参数文本>`：这可以不存在，但如果存在，它就是一系列 *记号* ，出现在 `<替换文本>` 之前。宏的 `<参数文本>` 可以包含宏参数（`#1`, `#2`... `#9`）以及其他记号类型。实际上，正如我们将详细看到的， `<参数文本>` 提供了一种 *记号模板* ，TeX 用它来确定用户想将哪些记号用作宏的实参：当你调用一个宏时，TeX 会将 *参数* 记号送入你的宏的 `<替换文本>` （它也是一个记号列表）；
* `{<replacement text>}`：这就是你宏的实际主体：它是一系列记号， *参数* 在宏被处理（展开）时，实参会被“注入”到其中。实参会被送入原始定义中所使用的宏参数所指示的位置。

**注意**：在整个讨论中，我们假定 `<宏名>` 之后会跟一个类别码为10的空格字符，作为终止 `<宏名>`的分隔符。 *没有* 在我们的正文/讨论中明确显示该空格字符，但我们假定它在那里。严格来说，我们应该像这样表示它：

```
<TeX macro primitive><macro name><space><parameter text>{<replacement text>}
```

不过，我们将省略显式包含一个 `<space>` 字符，并隐式假定它存在。

#### 关于 {} 的说明

我们已经展示了两对花括号的用法： `{` 和 `}` 它们包围（界定）你宏的实际主体 `<替换文本>`。 `{` 和 `}` 只是一种约定俗成的写法，因为 TeX 实际上期望的是你的宏文本 `<替换文本>` 以类别码1（“开始一个组”）的字符开头，并以类别码2（“结束一个组”）的字符结尾。按照惯例，这些是 `{` 和 `}` 字符，分别用于此目的。不过，如果你愿意，也可以指定任意一对字符来完成这件事。例如

```
\catcode`\(=1
\catcode`\)=2
```

现在，你可以这样定义并使用宏：

```
\def\foo #1(Hello, #1)
\foo(World!)
```

而且你仍然可以使用 `{` 和 `}` 因为我们并没有改变它们的类别码——多个字符可以而且确实可以具有相同的类别码，所以你仍然可以按通常方式定义宏：

```
\def\foo #1{Hello, #1}
\foo{World!}
```

### 和 {}

宏定义中与我们的讨论最相关的组成部分是 `<参数文本>` 和 `{<replacement text>}`。 `<参数文本>` 当你定义一个宏时， `<参数文本>` 可以为空，例如， `\def\foo{Some text}` 这里在我们的命令名（`\foo`）与左花括号 `{` 之间没有任何内容；在这里，左花括号标志着我们宏的 `<替换文本>`.

**注意**：有些读者可能了解 TeX 的“hashquote”机制（`#{`），但这里我们不展开讨论。

### 以及宏分隔符

本文并不是要对编写宏做一次彻底全面的回顾，不过做一个简短的复习，并给出一些示例，来展示 `<参数文本>` 也很值得，因为 TeX 允许 `<参数文本>` 包含：

* **宏参数**: (`#1`, `#2`,... `#9`）它们充当用户运行宏时提供的值的“占位符”——宏的 *参数*;
* **分隔符记号**：任意记号，穿插在参数记号之中或其周围，并用于指定宏参数之间的边界。你可以把这些分隔符看作构成一种“标点”，使得 `<参数文本>` 在使用宏时你需要遵循的“记号模板”。分隔符记号不会被排版。

那么这到底意味着什么——让我们看几个例子。 `<参数文本>` 位于宏名称和左花括号 `{` 之间，构成宏定义的一部分。

#### 分隔符：示例1

假设我们定义一个基本宏 `\foo` 如下：

```
\def\foo ABC{hello, there}
```

* `<参数文本>` = `ABC`
* 没有参数记号（`#1`, `#2`,... `#9`)
* 分隔符是 *三个字符记号* `ABC` 不过在这个例子中，它们有些多余，仅仅作为示例。

这三个字符记号 `ABC` 被视为分隔符：它们不是要排版的内容，而是宏被调用时预期出现的“标点”——当你使用宏 `\foo` 时，你必须提供定义时出现的相同分隔符。

如果你输入 `\foo ABC` 到文本中，这将排版出 `hello, there`——字符记号 `ABC` 不会被排版，但 TeX 确实非常仔细地检查以确保它们出现在你对 `\foo`的调用（调用）中。TeX 会查找这些分隔符（“标点”）并将其移除（吸收），如你在下面的 Overleaf 屏幕截图中所见： `ABC` 不会被排版：

![Overleaf 运行 TeX 宏](/files/4ae9f67ac233cd1c1f2f7257147636964afc66a9)

如果你尝试使用 `\foo` 而没有 `ABC` 分隔符，TeX 会报错 `对 \foo 的使用与其定义不匹配。`:

![在 Overleaf 上显示 TeX 错误](/files/570528cb3723cca5ec8dfa5d3ff1a33053446a5a)

在上面的例子中，在检测到 `\foo` TeX 预期看到这三个字符记号 `ABC` 但事实并非如此：它看到的是 `w` 的字符记号，并立即检测到有问题。

#### 分隔符：示例2

一个有趣、也许令人惊讶的事实是，分隔符可以是任意的命令记号——甚至包括那些 *甚至尚未定义*的命令。例如，我们可以定义 `\foo` 为：

```
\def\foo A\bob B\anne{Hello \TeX{}}
```

* `<参数文本>` = `A\bob B\anne`
* 没有参数记号（`#1`, `#2`,... `#9`)
* 分隔符是这些记号 `A\bob B\anne` 不过同样地，在这个例子中，它们是多余的，仅用于讨论。注意 `\bob` 和 `\anne` 是虚构的命令，它们 *未*定义——而且也不需要被定义。

当你调用一个宏时，TeX 会检查（扫描） `<参数文本>` 其中出现的 *在你的宏调用中* 并逐记号地将其与“记号模板”版本进行比较 *存储在内存中的*——也就是在定义宏时创建的那个版本。TeX 扫描 *你在宏调用中使用的* 并简单地将其中找到的任何命令转换为其数值记号值：它并不是要执行这些命令，因此 `\bob` 和 `\anne` 从未定义过也没关系。TeX 只是使用存储在内存中的记号模板作为指引，以便判断 `<参数文本>` 你宏调用中的哪些记号才是要送入 `<替换文本>`.

如你从下面的 Overleaf 屏幕片段中所见，字符记号（`一个` 和 `B`）在其中 `A\bob B\anne` 没有被排版，而我们未定义的命令 `\bob` 和 `\anne` 并没有造成任何问题。所有这些记号都被 *吸收* ，因为 TeX 在将你对 `\foo` 与……的定义（记号模板）进行匹配时 `\foo` （一个记号列表）存储在内存中。

![Overleaf 运行 TeX 宏](/files/c269ba2581140610db85c27c6d34a7db11491592)

#### 分隔符：示例3

你可以在宏参数之间穿插各种分隔符（字符记号、命令记号），如下例所示：

```
\def\foo A\bob#1B\anne#2\jane#3bye!{Hello from \TeX{} to \#1=#1, \#2=#2 and \#3=#3}
```

* `<参数文本>` = `A\bob#1B\anne#2\jane#3bye!`
* 有3个宏参数记号： `#1`, `#2`, `#3`
* 这一次，参数（`#1`, `#2`, `#3`）由多种记号组合来界定：

  `A\bob#1B\anne#2\jane#3bye!`

在这里，你实际上为 TeX 提供了一个模板，当你调用 `\foo`时，它会逐字记号地浏览你的宏调用， *逐记号地*并期望匹配（找到）：

* 这两个记号 `一个` 和 `\bob` 在 `#1`
* 这两个记号 `B` 和 `\anne` 在 `#2`
* 这个记号 `\jane` 在参数 `#3` 和 *前面的四个字符记号* `b`, `y`,`e` 和 `!` 在 `#3`.

**请记住**：TeX 是以记号来思考的，所以 `bye!` 是 *前面的四个字符记号*.

我们有两种方式使用这个宏——我们可以把任何多记号的实参放在花括号中，以提供一个组：

```
\foo A\bob{This}B\anne{That}\jane{Other}bye!
```

不过， *这并不是必需的* 因为我们的宏定义中有分隔符，提供了一个记号模板。TeX 可以利用该模板，从纯粹作为分隔符的记号中挑出每个实参的记号。我们可以这样使用这个宏：

```
\foo A\bob ThisB\anne That\jane Otherbye!
```

而 TeX 可以挑出这些实参，得到与使用组相同的结果 `{...}`:

![Overleaf 使用分隔符运行 TeX 宏](/files/618bc09c0c532ce1a998ceac17143fdf932f97e4)

注意 TeX 是如何精确检测出哪些记号与参数匹配的 `#1` 和 `#3`:

![Overleaf 使用分隔符运行 TeX 宏](/files/c5aa53515142991bf152d994f1917ead05be1119)

#### 分隔符：示例4（关键在于记号，而不是字符！）

这里有一个简短示例，说明在处理宏时，重要的是要记住我们真正处在 *记号* 和 **不** *字符*…

我们将定义如下简短宏，其中 A 和 B 是作为分隔符的字符，并且两者的类别码都是 11。这样可以正常工作：

```
\documentclass{article}
\begin{document}
\def\foo A#1B{Hello, #1}
\foo AGrahamB
\end{document}
```

并排版出 `Hello, Graham`.

不过，如果你将 A 或 B 的类别码改为除 11 之外的其他值，那么宏调用就会失败。假设我们使用下面的方式将 B 的类别码改为 12： ``\catcode`B=12`` 然后像之前一样再次尝试调用该宏：

```
\documentclass{article}
\begin{document}
\def\foo A#1B{Hello, #1}
\foo AGrahamB % This works
\catcode`\B=12\relax
\foo AGrahamB
\end{document}
```

它会以一个有些令人困惑的错误失败：

```
参数失控？
GrahamB \end {document}
！在扫描 \foo 的使用时文件就结束了。
<inserted text>
                \par
<*> main.tex

我怀疑你忘记了一个 `}'，导致我
读取到了你希望我停止的位置之后。
我会尝试恢复；但如果错误很严重，
你最好现在输入 `E' 或 `X' 并修正你的文件。

！紧急停止。
<*> main.tex

***（作业中止，未找到合法的 \end）
```

![Overleaf 显示 TeX 宏错误](/files/880c76f217b38a9cd0fba346446add6c68a896a3)

遗憾的是，TeX 默认给出的建议 ``我怀疑你忘记了一个 `}'`` 是 **不正确**，这并不是由缺少花括号（`}`).

#### 发生了什么？

TeX 正在试图匹配 `\foo` 宏 *调用* 与 `\foo` 宏 *定义* 它存储在内存中。当你调用一个需要一个或多个实参的宏时，TeX 必须检查（扫描）你对该宏的使用，以确定 *参数* 你为该宏提供了哪些实参。在这里，TeX 期望该实参夹在一个 A（类别码11）和一个 B（类别码11）之间。记住：TeX 是以 *记号*, **不** *字符*.

A 和 B 的记号值， $$\mathrm{T\_A}$$ 和 $$\mathrm{T\_B}$$ 分别是：

$$\mathrm{T\_A = 256 \times 11 + 65 = 2881}$$ $$\mathrm{T\_B = 256 \times 11 + 66 =2882}$$

不过，随着 TeX 继续搜索，它看到了 B *但是* ；现在它的类别码是 12，这就导致了一个不同的记号值：

$$\mathrm{T'\_B= 256 \times 12 + 66 =3138}$$

在 `\foo` 宏定义中，B（用作分隔符）的记号值是 2882，但 TeX 现在看到的记号值是 3138：它认为这只是另一个注定要在 *参数* 中使用的记号，供 `\foo`。在 TeX 看来，实参的最后一个记号还没有找到，因此它会继续获取下一个记号，寻找类别码为 11 的 B。也正是在这时、也正因为如此，宏失败了：在试图找到该实参时，TeX“越界”了，并开始读取那些你并不打算让它们成为 `\foo` 宏调用一部分的记号。接下来会发生什么，取决于 TeX 在 B 之后发现了哪些记号——它们会触发各种 `参数失控？` 错误。

在我们的例子中，TeX 越界并吞掉了这串记号 `\end {document}` 并迅速到达文件末尾，因此出现错误信息：

```
！在扫描 \foo 的使用时文件结束了
```

## 第 5 部分

正如我们上面所看到的， `<参数文本>` 宏定义中的部分可以从极其简单到一种复杂的混合形式，其中宏参数穿插着作为分隔符的字符记号和命令记号。TeX 能够处理 `<参数文本>` 中存在的各种记号组合，以提取为宏提供的实参——并在我们错误使用宏时进行检测。它是如何做到这一点的，将是本系列下一篇文章的主题。

[第 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/22-how-tex-macros-actually-work-part-4.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.
