\begingroup\expandafter…\endgroup 起什么作用?

\begingroup\expandafter…\endgroup 起什么作用?

我经常在包实现中看到以下形式的代码(此示例来自 LaTeX3 源):

\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname directlua\endcsname\relax
\else
\fi

第一行包含三个\expandafters,这让我很困惑。我只能理解到这一步:

  • \begingroup创建一个群组
  • \expandafters链导致\csname directlua\endcsname转换为控制序列
  • 在此之后,状态是我们在一个组中,并\expandafter\endgroup\ifx[directlua]\relax…继续接受宏处理器的检查([directlua]表示控制序列)
  • \expandafter现在处理最后一个,并\ifx进行扩展,然后\endgroup结束该组。电子书关于这个话题,我是这样说的:

    \if…被扩展时,TeX 会尽可能向前读取,以确定条件是真还是假;如果为假,它会向前跳过(跟踪\if…\fi嵌套),直到找到\else\or\fi来结束跳过的文本。同样,当\else\or\fi被扩展时,TeX 会读取到任何应该跳过的文本的末尾。条件的“扩展”为空。

    这表明参数\if…在组内进行评估。但是条件分支内的代码怎么办?

如果所讨论的代码的目的确实是评估组内部,那么为什么它比仅在和\if…之间插入条件更好?\begingroup\endgroup

答案1

让我们一步一步来看

\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname directlua\endcsname\relax
  A
\else
  B
\fi

这变成

(\begingroup)\expandafter\endgroup
\ifx\directlua\relax
  A
\else
  B
\fi

已经\begingroup消化过了,所以我把它放在括号里,只是为了记住已经打开了一个组。现在,下一步,我们必须区分不同情况。

情况 1:\directlua未定义,因此 产生的标记\csname directlua\endcsname相当于\relax

(\begingroup)\endgroup A\else B\fi

现在被消化了,这就消除了对 的\endgroup意义的分配。经过检查, 的扩展是空的。\relax\directluaA\else B\fi

情况2:\directlua已定义。

(\begingroup)\endgroup B\fi

再次\endgroup被消化,但没有恢复任何东西。 的扩展\fi是空的。

\directlua为什么不在小组内这样做?关键是最后不是如果它不在进程开始时定义,则同样如此。如果代码是

\begingroup\expandafter\ifx\csname directlua\endcsname\relax A\else B\fi\endgroup

A但是和 的目的B是进行一些赋值。在这种情况下A可能是\luatexfalse,在之前说过 之后\newif\ifluatex, 和B将是\luatextrue。组内的三元组\expandafter免除了全局赋值,遵循对变量的赋值应始终是全局的或始终是局部的良好做法(只要可能)。当然,在这种情况下,全局赋值并不那么重要,在其他情况下,它可能会对保存堆栈产生影响。

建议的替代方案

{\expandafter}\expandafter\ifx\csname directlua\endcsname\undefined
  A
\else
  B
\fi

(与\undefined,而不是\relax)不太有吸引力,因为它依赖于某个未定义的标记。有人可能会反对我们正在分析的代码假设\relax有其原始含义,但有些假设需要被制造。

如果可以假设 e-TeX,则更简单的测试

\ifdefined\directlua
  A
\else
  B
\fi

甚至可以完全扩展。

答案2

原因是,如果 不存在,则\csname ...\endcsname会定义...为等于 的宏\relax。此功能用于将\ifx其与 进行比较。如果之前未定义(或 为),\relax则此测试为真。...\let\relax

\relax但是,定义宏甚至只是为了测试其存在性都不是好的做法。e-TeX 提供\ifcsname ...\endcsname了此功能。如果没有 e-TeX,则可以将 a 组与 一起使用来\expandafter处理\csname\ifx内部的 ,以保持宏定义在本地。

\if...请注意,当真的TeX 只是继续处理以下标记。它记得寻找\fi被删除的结束符或\else应该跳过的分支。如果测试错误的直到 的所有内容\else都会立即被跳过,TeX 会再次记住寻找一个结束的\fi。因此,所有的\expandafter技巧都发挥了很好的作用。\ifx被展开,TeX 已经选择了要执行的分支。然后 被\endgroup插入,并执行该分支。

将整个表达式包装在组内的好处非常明显:实际内容可以定义/更改本地设置!

请注意,在 TeX 中,if 语句和组是独立的(在几乎所有其他编程语言中都不是这种情况)。因此,您还可以编写以下内容以保持语句的\csname ...\endcsname本地性:

\begingroup
\expandafter\ifx\csname directlua\endcsname\relax
    \endgroup
\else
    \endgroup
\fi

\endgroup这里只会执行一次。

答案3

正如上面的一些评论中所提到的,关于“哈希表”和“等价表”等的一些额外的说明/演示。

哈希表

哈希表(或者hash_extra,很复杂)将每个控制序列名称映射到一个数字(以便快速操作标记等)。

在 LuaTeX 中,可以使用以下方法检查某个 token 是否在哈希表中:token.get_next()或者token.create()

在下面的例子中,最初\undefineda并不在哈希表中,但在它被“看到”之后(即使它从未被定义)它就在哈希表中。(您还可以看到,\ifdefined即使标记被 TeX“读取”,它本身也不会创建新的哈希表条目)

%! TEX program = lualatex
\documentclass{article}
\usepackage{luacode}
\begin{document}

\begin{luacode*}
print("\n\n\n\n")

function f()
    print("f: csname =", token.get_next().csname)
end
\end{luacode*}

\ExplSyntaxOn

\directlua{f()} \undefineda  % here the output is empty, so \undefineda is not in the hash table

\ifdefined\undefineda \directlua{print "defined"} \else \directlua{print "undefined"} \fi
\directlua{f()} \undefineda  % special case: \ifdefined (alone) does not put the token into the hash table

\iffalse \undefineda \fi
\directlua{f()} \undefineda  % special case 2: \iffalse ... \fi does not put the token into the hash table

\use_none:n {\undefineda}    % the token appears, but \use_none:n "throws it away" immediately
\directlua{f()} \undefineda  % now undefineda is in the hash table

\ExplSyntaxOff

\begin{luacode*}
print("\n\n\n\n")
\end{luacode*}

\end{document}

Relevant output:

f: csname =
undefined
f: csname =
f: csname =
f: csname =     undefineda

等价表

“等价表”是 TeX 的一个实现细节,在texdoc tex它里面被称为“等价表”,第 17 部分,第 220 节;而在源代码中它是变量eqtb

正如文档所述,它保存着事物的当前“等价物”。

请注意(实现细节),该表不称为“宏定义表”,因为它包含的不仅仅是宏定义,还有

  • 控制序列/活动字符可以定义为宏以外的其他内容(例如:

    • \chardef \% = `%这使得执行时\%等效于(不是“扩展为”!它仍然不可扩展) ;\char `%
    • \let \c_math_toggle_token = $
    • ETC。)
  • 例如,baselineskip 的值也放在这个表中。

简而言之,如果我理解正确的话,“在等价表中不存在\directlua”只是“未定义”的一种复杂说法。\directlua

注意\let \directlua \undefined并保存堆栈

在这种特殊情况下,如果您执行\begingroup,则执行\csname directlua \endcsname然后执行,这与在之后\endgroup执行类似- 唯一的区别(据我所知)是在“保存堆栈”中,即,如果您单独执行,它将在保存堆栈中留下一个项目:\let \directlua \undefinedcsname-endcsname\let \directlua \undefined

%! TEX program = lualatex
\documentclass{article}
\begin{document}

\ExplSyntaxOn

\begingroup

\tracingrestores=1~

\wlog{========~first~example}

\begingroup


\begingroup \expandafter \endgroup \expandafter \use_none:n \csname testtest \endcsname

\wlog{//there's~a~restore~above~because~of~the~endgroup,~but~no~restoration~below}

\endgroup

\wlog{========~second~example}

\begingroup

\expandafter \use_none:n \csname testtest \endcsname
\let \testtest \undefined

\wlog{//note~that~there's~still~a~restoration~below}

\endgroup

\endgroup

\ExplSyntaxOff

\end{document}

The relevant output of this program is:

======== first example
{restoring \testtest=undefined}
//there's a restore above because of the endgroup, but no restoration below
======== second example
//note that there's still a restoration below
{restoring \testtest=undefined}
{restoring \tracingrestores=0}

side note, \use_none:n is abused to gobble a single token here, but not a big problem

添加

正如下面的评论所示egreg 的回答 我有点困惑为什么他们不这么做\ifx\directlua\undefined

我的猜测是,LaTeX3 团队知道\csname...\endcsname(以及使用它的各种其他构造)会自动将控制序列定义为\relax最初未定义时的状态,认为这容易出错,并决定向用户隐藏该行为,并且\relax以同样的方式对待和未定义

\directlua尤其是在这种情况下,即使用户之前在某个地方意外做了(内部做了某件事),代码也会认为“不存在” \csname directlua \endcsname。我认为这不太可能。

将其与\cs_if_exist:NTF或 的\cs_if_exist:cTF工作方式进行比较。虽然\ifdefined\ifcsname基元在 上为真\relax而在 上为假undefined,但上述两个命令都被硬编码为在 上为假\relax

(值得注意的是,与许多其他采用 c 类型参数的命令不同,这些命令是通过\exp_args:Nc或实现的\::c,此命令才不是隐式地将控制序列定义为,\relax因为undefined 它是使用 的特殊情况实现的\ifcsname;尽管如此,这种行为(以及大多数但不是全部 c 类型参数隐式地将未定义转换为\relax,或在其 LaTeX3 名称中 ,\scan_stop:)据我所知从未在 interface3.pdf 的任何地方记录过)

话虽如此,我的观点(但与这个问题并没有什么关系)是,这永远不可能完全从用户那里抽象出来,因为未定义的控制序列在扩展/执行时会产生错误,而\relax控制序列则不会。

相关内容