我经常在包实现中看到以下形式的代码(此示例来自 LaTeX3 源):
\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname directlua\endcsname\relax
\else
…
\fi
第一行包含三个\expandafter
s,这让我很困惑。我只能理解到这一步:
\begingroup
创建一个群组\expandafter
s链导致\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
\directlua
A
\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 \undefined
csname-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
控制序列则不会。