\@firstoftwo 和 \@secondoftwo 起什么作用?

\@firstoftwo 和 \@secondoftwo 起什么作用?

LaTeX 内核到处都使用这两个。例如,显然定义类似内容的正确方法\ifeq{\macro 1}{\macro 2}{true}{false}是(注意 Martin 的回复:每个案例都应以 开头\expandafter

\def\ifeq#1#2{%
 \ifx#1#2\relax
  \@firstoftwo
 \else
  \@secondoftwo
 \fi
}

而不是

\def\ifeq#1#2#3#4{%
 \ifx#1#2\relax
  #3%
 \else
  #4%
 \fi
}

然而,据我所知,它们是等效的:

\long\def\@firstoftwo#1#2{#1}
\long\def\@secondoftwo#1#2{#2}

甚至还有一个\@firstofone! 它的作用是:

\long\def\@firstofone#1{#1}

显然,使用这些宏与简单地声明一个或两个参数没有任何功能差异。我一眼就能看出两个可能的小差异:

  1. \@firstoftwo%是一个控制序列,因此会占用后续空格。这意味着如果它结束一行,我们不必在它后面放置一个。当然,我们必须%在很多其他地方放置一个,而且 TeX 实际上不会将一个存储%在宏的扩展文本中,所以这确实是次要的。

  2. 用 定义的宏\@firstoftwo比通常定义的宏少接受两个参数。因此,TeX 不必存储我的宏接受这两个参数的信息;它只需引用和\ifeq已经使用的存储。我想这可以减少一点内存使用量。 这两个都不能解释 的用途,因为它只有在作为宏的最后一个标记放置时才可能有用,即使在那里它也不会真正改变后续输入,所以似乎可以将其消除。是否有某种原因会想要这样“设置”下一个标记?还有一种可能性:\@firstoftwo\@secondoftwo

    \@firstofone

  3. 这些是\long\def'd 而不是\def'd。因此,即使使用它们的宏不是 ,它们也可以接受多段参数\long。我猜这允许您在其他宏不是时创建最后一个或两个参数,但实际上,为什么不在您真正想要的时候\long使用呢?\long

我还遗漏了其他优点吗?它们对速度的影响很小,还是与扩展或执行的互动很棘手,而我却忽略了?真实的造成这种混淆的原因是什么?

答案1

您遗漏了两个非常重要的\expandafters。通常使用的“正确”代码是:

\def\ifeq#1#2{%
 \ifx#1#2\relax
   \expandafter\@firstoftwo
 \else
   \expandafter\@secondoftwo
 \fi
}

#3与使用和的宏的区别#4在于,在处理接下来两个参数中的第一个或第二个参数之前,if 语句已完全处理完毕。这允许该代码例如使用 向前看\@ifnextchar。否则\else\fi仍然分别位于#3或之后#4,这会破坏此类宏。这种首先关闭 if 语句的技术对于递归宏来说也很重要,因为它不会积累大量级联的 if 语句。

宏定义\long很简单,以便能够最灵活地使用。


\@firstofone宏看似无操作,但实际上删除了参数周围的括号。它通常用于 if 语句中,其中\@gobble在另一个子句中使用(两者再次使用\expandafter)。

\def\foo#1#2{%
 \ifx#1#2%
   \expandafter\@firstofone
 \else
   \expandafter\@gobble
 \fi
}

\foo{\bar}{\baz}{Argument code}

答案2

正确的语法\ifeq应该是

\def\ifeq#1#2{%
 \ifx#1#2\relax
  \expandafter\@firstoftwo
 \else
  \expandafter\@secondoftwo
 \fi
}

马丁很好地解释了选择这种方法而不是“四个论点”方法的原因。

那 呢\@firstofone?嗯,例如,它可以用来去除参数周围的一对括号。另一个用途是使某事成为无用操作;在 LaTeX 内核中,有

\let\AtBeginDocument\@firstofone

它作为宏的一部分执行\document;因此,如果添加了带有\AtBeginDocument调用本身的东西\AtBeginDocument,则将执行此命令的参数(而不是括号中的参数)。这个例子可能很愚蠢,但在现实世界中可能会发生这种情况:

\AtBeginDocument{\AtBeginDocument{do something}}

如果内核说\let\AtBeginDocument\relax,结果将是喂令牌

{do something}

代码中的和赋值将在组结束时丢失。如果定义正确,LaTeX 将显示\@firstofone{do something}扩展为的do something,而无需括号。

当然没有人会写这样的代码;但是包可能会说

\AtBeginDocument{\macro}

而另一个包可能已经\macro通过调用重新定义了\AtBeginDocument

另一个例子:我们想要一个宏,当以正常形式调用时,它不会对其参数执行任何操作,但如果以 * 形式调用,则以斜体打印它。

\newcommand{\macro}{\@ifstar{\textit}{\@firstofone}}

然后\macro{x}将打印 x,同时\macro*{x}将打印X。可以省略\@firstofone,但这不会删除 的(明显)参数周围的括号\macro。在这种情况下,习惯上使用\@iden,它与 相同\@firstofone

还有另一种情况\@firstofone非常有用,请参阅 Bruno Le Floch 的回答“双字母字体样式命令(\bf,,\it...)会在 LaTeX 中复活吗?

答案3

由于之前的一些回答已经讨论了 的用法\@firstofone,我也会讨论。一种用法是,当\@firstofone展开时,其参数的 catcode 是固定的。因此,

\begingroup
\makeatletter
\catcode`\*=\active
\@firstofone{\endgroup
  \newcommand{*}}{*}

*定义要扩展为“其他”字符的活动字符*。这里,\makeatletter是必需的,因为\@firstofone包含一个@,并且我们将其放在组内:这样,\endgroup一旦\@firstofone扩展,读取时所有的 catcode 都会被还原。

答案4

其他一些观点,我认为其他问题中没有提到。

(*)这个答案中有很多虚假的空间,它们仅仅是为了演示目的;或者假设它们在 expl3 的\ExplSyntaxOn环境中。

TeX 条件究竟如何工作?

如果您没有仔细阅读(TeX)书,而只是浏览命令描述以了解它们的作用,您可能会认为......

\ifx #1#2
    ⟨some code⟩
\else
    ⟨some other code⟩
\fi

⟨some code⟩... 将执行与或完全相同的操作,⟨some other code⟩取决于前两个标记是否相等。

但是,如果你仔细想想,这些条件是可扩展的。它们如何实现呢?

一个合理的猜测/“心理模型”是

  • \ifx抓取下一个 token,直到看到\fi
  • 然后比较前两个 token 是否相等
  • 然后它“返回”⟨some code⟩或者⟨some other code⟩被执行。

因此在上面的例子中,它将在 1 步内扩展为 或⟨some code⟩⟨some other code⟩有道理,对吧?

主要问题是,这个模型是错误的。现实是……

  • 当 TeX 看到 时\ifx,它会取出 2 个下一个标记并进行比较。
  • 如果比较为真,它会“记住” “这在一个真正的分支内。当\else看到下一个时,吞噬直到下一个\fi;或者当\fi看到下一个时,将其移除”。
  • 如果比较结果为假,它会一直吞噬直到出现\elsefi,然后(可选,如果\else出现)记住“这是在错误分支内。当\fi出现下一个时,将其删除”。

例如,如果条件为真,则经过 1 步扩展后,结果为

    ⟨some code⟩
\else
    ⟨some other code⟩
\fi

扩展处理器记住这是真分支。查看\else代码中剩余的不平衡。

你可以自己尝试一下,执行\iftrue\show\else\fi\a,就会显示\else

(也在 TeXbook 中讨论过,第一段“条件”,第 20 章第 213 页 / TeX by Topic,第 13.7 章条件评估 / TeX 简介,第 16 页(简要)。)

为什么?这违反直觉!

我猜测这种行为有以下几个原因:

  • 首先,因为标记没有提前标记,所以您可以\verb在某些分支内使用 catcode 更改命令(例如)。(大多数情况下)。
  • 您不需要将整个 2 个分支读入内存,它们可以稍后读取。这可能是几十年前的主要原因。

如果代码没有提前查找下一个标记会怎么样?

(替代标题:T E X 原始条件)

仍存在一个潜在问题。

假设用户提供#3#4具有一些 TeX 原始条件,它们可能会不平衡:

\ifx #1#2
    \fi
\else
    ⟨some other code⟩
\fi

您可以在这里看到问题。使用\@firstoftwo/方法时不会发生这种情况。\@secondoftwo

附注:您不能对这个进行大括号保护,在这种情况下,TeX 条件会忽略大括号。(因此有了著名的“大括号破解”\iffalse { \fi }\expandafter { \iffalse } \fi

附注:代码可以进一步优化。

如果你执行以下操作

\def     \change@secondoftwo@to@firstoftwo  \fi \@secondoftwo #1#2 {
                                            \fi               #1
                                        }
\ifx #1#2 \change@secondoftwo@to@firstoftwo \fi \@secondoftwo {#3} {#4}

你只需要 3 个 token,而不是通常的 5 个\expandafter \@firstoftwo \else \@secondoftwo \fi(方法在https://tex.stackexchange.com/a/517265/250119。

expl3 代码与宏\__prg_TF_true:w等执行类似操作。

附注,您需要插入\fi后面的内容,因为如上所述,扩展处理器会记住“我们在真正的分支中”并且\end occurred when \ifx was incomplete最终会打印例如“ ”错误消息。 (由于某种原因,它实际上不是错误消息,只是打印到终端的警告。)

相关内容