脆弱命令和坚固命令之间有什么区别?什么时候以及为什么我们需要 \protect?

脆弱命令和坚固命令之间有什么区别?什么时候以及为什么我们需要 \protect?

所以我读过几次关于这个、关于“移动参数”和所有这些东西的文章。但这到底是什么意思?有人能用几句话解释一下 TeX 级别发生了什么,脆弱命令和强健命令之间有什么区别吗?此外,我们什么时候需要使用\protect以及为什么?

答案1

这里的关键概念是,当 TeX 处理输入时,它会做两件不同的事情,分别称为扩展执行东西。通常,这些活动是交错的:TeX 接受一个标记(即,输入的基本部分),扩展它,然后执行它(如果可能)。然后它对下一个标记执行同样的操作。在某些情况下,最明显的是,在写入文件时,TeX 只会扩展内容而不执行它们(当 TeX 读回文件时,结果很可能会被(重新扩展并)执行)。某些宏的正常运行依赖于某些内容的正确执行下一个标记被扩展。这些标记被称为“脆弱的”,因为它们只能在正常(交错)模式下工作,而不能在仅扩展的上下文中工作(例如“移动参数”,这通常意味着写入文件)。

这就是总体情况。现在让我们给出“一些”更多细节。请随意跳到“实践中该怎么做” :)

扩张与执行

之间的区别扩张执行有点武断,但根据经验法则:

  • 扩张变化仅有的输入流,即“TeX 接下来要读取什么”;
  • 执行力就是一切。

例如,宏是可扩展的(TeX 接下来将读取它们的替换文本),\input可扩展的(TeX 接下来将读取给定的文件)等等,\def不可扩展的(它改变了定义的宏的含义),\kern不可扩展的(它改变了当前段落或页面的内容)等等。

事情如何出错

现在,考虑一个宏\foo

\newcommand\foo[1]{\def\arg{#1}\ifx\arg\empty T\else F\fi}

在正常上下文中,\foo{}给出Tfoo{stuff}给出F。)在正常上下文中,TeX 将尝试扩展\def(不执行任何操作)然后执行它(\arg{#1}从输入流中删除并定义\arg),然后扩展下一个标记\ifx(删除并可能删除直到输入流中的\arg\empty匹配项的所有内容,但不包括匹配项),等等。\else

在仅扩展上下文中,TeX 会尝试扩展\def(不执行任何操作),然后扩展接下来的内容,即\arg。此时,任何事情都可能发生。也许\arg未定义,因此您会收到一条(令人困惑的)错误消息。也许它被定义为 之类的东西abc,因此\foo{}将扩展为\def abc{} F。将其写入文件时您不会收到错误,但读回时会崩溃。也许\arg定义为\abc,然后\foo{}会扩展为\def\abc{} F。然后,无论是写入还是读回,您都不会收到错误消息,但是您不仅会得到F您所期望的T,而且还会\abc得到 被重新定义,如果这是一个重要的宏,则这可能会产生各种后果(祝您好运能够追踪到这个错误)。

保护如何发挥作用

编辑以添加(不是原始问题,但有人在评论中问到):那么如何工作\protect?好吧,在正常情况下,\protect扩展为\relax不执行任何操作。当 LaTeX(不是 TeX)命令即将在仅扩展模式下处理其参数之一时,它会更改\protect为基于的含义\noexpand,这避免了下一个标记的扩展,从而保护它免于被扩展但不执行。(有关完整详细信息,请参阅 source2e.pdf 中的 11.4。)

例如,使用\foo如上所述的方法,如果您尝试,\section{\foo{}}则会出现如上所述的混乱。现在,如果您这样做,\section{\protect\foo{}}那么当 LaTeX 打印章节标题时,它处于正常(交错)模式,\protect扩展为\relax,然后\foo{}正常扩展和执行,并且您会在文档中看到一个大 T。在 LaTeX 将章节标题写入.aux目录文件之前,它会更改\protect\noexpand\protect\noexpand,因此\protect\foo会扩展为\noexpand\protect\noexpand\foo\protect\foo写入辅助文件。当将辅助文件的该行移动到 toc 文件时,LaTeX 定义\protect\noexpand,因此只会\foo写入 toc 文件。当最终以正常模式读取 toc 文件时,才\foo进行扩展和执行,并且您会再次在文档中看到一个 T。

你可以使用下面的文档,查看不带 和带有 的.aux和文件的内容。注意:(1)你需要手动运行文件,而不是或你的 IDE,后者可能一次运行多次,(2)在尝试非 ed 版本后,你将需要删除 toc 文件进行恢复。.toc\protectpdflatexlatexmk\protect

\documentclass{article}
\newcommand\foo[1]{\def\arg{#1}\ifx\arg\empty T\else F\fi}
\begin{document}
\tableofcontents
\section{\foo{}} % first run writes garbage to the aux file, second crashes
%\section{\protect\foo{}} % this is fine
\end{document}

有趣的事实:如果我们在 的定义中用替换 的每个出现,\arg则不受保护的版本将以不同的方式失败(如上所述)。\lol\foo

哪些宏是脆弱的

这是问题中比较简单(但定义明确)的部分。现在,困难的部分是:何时使用\protect?这要视情况而定。如果不查看其实现,您就无法知道宏是否脆弱。例如,\foo上面的宏可以使用可扩展技巧来测试是否为空,并且不会脆弱。此外,一些宏是“自发的”(例如,\protect用 定义的宏)。正如 Joseph 提到的,除非您(或另一个包)加载,否则它是脆弱的。(根据经验,大多数数学模式宏都是脆弱的。)此外,您无法知道特定宏是否尝试仅扩展其参数,但您至少可以确保所有移动参数在某个时候都会仅扩展。\DeclareRobustCommand\(fixltx2e

实践中该怎么做

因此,我的建议是:当您看到移动参数中或附近发生奇怪的错误时(即移动到文档另一部分的一段文本,如脚注(移动到页面底部)、章节标题(移动到目录)等),请尝试\protect执行其中的每个宏。这可以解决 99% 的问题。

(当你将这个技巧应用到同事的文章上时,你就会成为英雄,因为今天要交的文章却“神秘地”崩溃了:看他们的文档几秒钟,然后你会看到\section标题里有一个数学公式,说“\protect在这里添加一个”,然后回去工作,让他们称你为巫师。这是一个廉价的伎俩,但很有效。)

答案2

这里的关键概念是扩展。我将以一个假设的“脆弱”函数为例\foo,用于以下论证\section

\section{Some text \foo[option]{argument}}

当 LaTeX 处理\section宏时,它会执行许多操作。其中之一就是将节名称的文本写入文件.aux。现在,这里的关键点是它\write有效地使用了原语:

\immediate\write\@auxout{Some text \foo[option]{argument}}

\write语以与 相同的方式扩展其参数\edef。但是,我说过 是\foo“脆弱的”。这意味着尝试\edef这样做要么会导致错误,要么会导致错误的结果。一个典型的例子是任何带有可选参数的宏:这些宏的检测不能在 内扩展\edef。另一种情况是,某些东西将根据其在输入中的位置进行编号,这可能会导致输出中的编号错误。例如,参见http://texblog.net/help/latex/fragile.html有关脆弱宏的更多详细信息(但请注意fixltx2e包对其中的一些进行排序)。

当您使用 时\protect,它会阻止 TeX 在 期间扩展下一个标记\write。因此文本将“按给定”写入文件.aux。这当然要求您知道哪些函数需要保护。正如 TH 所指出的,它还需要正确使用\protected@write\protected@edef才能正常工作。(这些宏的工作方式是通过改变 的定义来\protect实现所需的效果。因此在 内部\protected@edef, 的扩展\protect\noexpand\protect\noexpand,例如。)

\DeclareRobustCommand在 LaTeX2e 中可用。这会在宏本身中添加一些自动保护,因此\protect不需要这样做。这在某些\protected@write情况下也有效。

这很好,但是更好的方法是 e-TeX 的\protected系统:

\protected\def\foo....

以这种方式定义的宏根本不会在或内部展开\edef\write因为引擎本身知道不要管它们。这是etoolboxxparse用于定义真正强大的宏。受引擎保护的宏完全不依赖 LaTeX2e 的机制,因此在普通的 中是安全的\edef

答案3

这个问题也许最好通过一个例子来回答。考虑一下fragrance命令\title。以下是来自的相关定义latex.ltx

\def\title#1{\gdef\@title{#1}}
\def\@title{\@latex@error{No \noexpand\title given}\@ehc}

\title{This is the title}现在想象一下,您在命令的参数中包含一个首先执行此参数的命令,然后将其写入辅助文件,或以其他方式“移动”它。我们可以运行一个交互式实验来查看会发生什么,使用\edef立即查看结果:

; latex
This is pdfTeX, Version 3.1415926-1.40.11 (TeX Live 2010)
 restricted \write18 enabled.
**\relax
[…]
*\title{This is the title}

*\edef\foo{\title{This is the title}}

*\show\foo
> \foo=macro:
->\gdef This is the title{This is the title}.
<*> \show\foo

正如您所想象的(如果必须,请尝试一下),实际执行\foo不会像您在运行此实验之前所预期的那样进行。当然,问题是这个问题\@title扩大了。

要想知道它是如何\protect工作的,你可以做比跑步更糟糕的事情texdoc source2e并查看第 11.4 节,强大的命令和保护

答案4

一些评论。

注意旧式强命令。

因为历史原因(?),一些命令是“旧式健壮的”(通常是在 e-TeX 可用之前写回的命令),一些是“新式健壮的”(使用 e-TeX 的\protected机制)。另请参阅这个问题

你可以分辨出哪一个是旧式健壮,哪一个是新式健壮,例如:

$ latexdef textbf

\textbf:
macro:->\protect \textbf  


\textbf :
\long macro:#1->\ifmmode \nfss@text {\bfseries #1}\else \hmode@bgroup \text@command {#1}\bfseries \c
heck@icl #1\check@icr \expandafter \egroup \fi 

如果有\protect,那么它就是旧式健壮的。

只有少数新型强健命令可用:

$ latexdef lfloor

\lfloor:
\protected macro:->\delimiter "4262304 

$ latexdef AddToHook

\AddToHook:
\protected macro:->\__cmd_start:nNNnnn {mo+m}\AddToHook  \AddToHook code {\__cmd_grab_m_1:w \__cmd_g
rab_D:w []\__cmd_grab_m_long:w }{}{}

您可以通过 来查看它们\protected

问题是,如果某个命令是老式的健壮命令,那么不能在内部使用\write\edef

相反,分别有\protected@write\protected@edef。但是没有\protected@immediate@write ,您需要自己定义一个。

命令如\hyphenation

$ latexdef \hyphenation

\hyphenation:
\hyphenation

是“坚固的”(不可扩展的),因为它们是TeX 原语,所以他们在任何地方都是安全的。

对于用户定义的命令,\NewDocumentCommand会产生新式健壮命令,(几乎)其他所有内容都会产生旧式健壮命令。

$ latexdef -o '\DeclareRobustCommand\test{hello}' test  #% this is old-style robust because it has "\protect"

\test:
macro:->\protect \test  


\test :s?
\long macro:->hello

$ latexdef -o '\newcommand\test{hello}\MakeRobust\test' test  #% identical to the above

\test:
macro:->\protect \test  


\test :
\long macro:->hello

$ latexdef -o '\NewDocumentCommand\test{}{hello}' test  #% this is new-style robust because it has "\protected macro"

\test:
\protected macro:->\__cmd_start_expandable:nNNNNn {}\test  \test  \test code ?{}

$ latexdef -p etoolbox -o '\newrobustcmd\test{hello}' test  #% this is new-style robust

\test:
\protected\long macro:->hello

$ latexdef -o '\newcommand\test{hello}' test  #% this is not robust

\test:
\long macro:->hello

并非所有命令都是脆弱的或强大的。

有以下几种命令我们不会说它们是脆弱的或者健壮的:

  • \verb或类似的命令,无论如何,它在命令参数中都不会起作用\protect

    (备注:这些命令改变了猫码某些字符。您需要cprotect或类似的,或者只需切换到等效的非 catcode 更改命令,例如\texttt)

  • 完全可扩展的命令,即旨在“返回结果”而不是排版/执行某些操作的命令。

    无论是在仅扩展上下文还是在执行上下文中,此功能始终都会起作用。

定义。

(以“总体概述”术语来说,而不是以 TeX 原语术语来说。)

上面的答案很好地定义了脆弱的命令。

某些宏的正确操作依赖于在下一个标记展开之前正确执行某些操作。这些宏被称为“脆弱的”,因为它们仅在正常(交错)模式下工作,而不在仅展开的上下文中工作(例如“移动参数”,这通常意味着写入文件)。

换句话说,如果

  • 它在排版环境中起作用
  • 在仅扩展环境中使用时,它无法按预期工作。

相反,一个强有力的命令

  • 在排版环境中工作
  • 根本没有扩展在仅扩展上下文中。

(请记住,完全可扩展的命令既不脆弱也不强大。)

每个脆弱的命令都可以变得强健。

因此,原则上,使命令变得健壮并不是很难——只需延迟扩展它,直到它处于执行上下文中。

(作为比较,并非所有不可扩展的命令都可以扩展。)

如果你遇到“神秘错误”一些 LaTeX 内置命令,这是因为例如 LaTeX 团队没有使其变得健壮,很可能是因为历史原因。

(几十年前,需要一些额外的内存字节来增强命令的可靠性,而机器的内存是有限的。如今大多数命令已经变得强大,所以这种情况希望很少见。)

如果这是您自己的命令,请使用\DeclareRobustCommand\NewDocumentCommand代替\def\newcommand希望能解决问题。

或者如果它与其他一些库一起定义,例如tcolorbox 的\newtcbox其中\NewDocumentCommand不是一个选项,而是\MakeRobust(参见上面链接的答案中的示例。文档不知道在哪里,但它是 LaTeX 内置的),或etoolbox\robustify

为什么必须\section充分展开其论点?

或者:\section如果只是将内容写入未扩展的文件中,然后稍后执行它,不是更简单吗?

以前,我认为这是不可能的。(至少没有一些额外的编程努力,而且用 TeX 编程很困难。)

如今,在 e-TeX 中可以 \detokenize写入文件之前的参数,不会扩展任何内容;但是......

作为埃格指出除非用户明确注入扩展值,否则这将导致用户定义的计数器的值不正确,而这并不容易。

还引用了 Cyrus 在该答案下面的评论

TeX 允许用户扩展,但如果用户不想要,他们可以选择不扩展,使用\protect\DeclareRobustCommand。默认是扩展,所以需要额外的工作如果用户有其他需要的话。

相关内容