间接定义的宏:带有“@”的未定义宏不会触发编译错误

间接定义的宏:带有“@”的未定义宏不会触发编译错误

我定义了两个宏\newfoo{#1}{#2}\foo{#1}。第一个参数用作键,必须是有效的 LaTeX 宏名称(不带反斜杠)。第二个参数是一小段文本。使用\newfoo键与其文本相关联,并且使用\foo文本应该被打印,给定先前定义的键。

对于熟悉 glossary/-ies 包的人来说,这些命令最好被描述为那些命令的穷人版本,但我在其他情况下需要它们。

在内部,\newfoo{#1}{#2}定义一个宏\foo@#1\foo{#1}扩展为\foo@#1。下面的 MWE 可以工作。但是,如果\foo为之前未定义的键调用,我本来预计会出现编译错误,因为\foo@#1不存在。然而,编译默默进行,只是将不存在的扩展\foo@#1为空。这种行为是不可取的,因为应该注意拼写错误的键。

\documentclass{article}

\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage[utf8]{inputenc}

\makeatletter

\newcommand*\newfoo[2]{%
\expandafter\newcommand\csname foo@#1\endcsname{#2}%
}

\newcommand*\foo[1]{\csname foo@#1\endcsname}

\makeatother

\newfoo{a}{1st letter of alphabet}
\newfoo{b}{2nd letter of alphabet}

\begin{document}

\verb#\foo{a}# is expected to expand to ``\foo{a}''

\verb#\foo{b}# is expected to expand to ``\foo{b}''

\verb#\foo{c}# has not been defined by \verb#\newfoo{c}{...}# and should raise a compile error

\end{document}

答案1

解决方案是

\newcommand*\foo[1]{%
  \ifcsname foo@#1\endcsname
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
  {\csname foo@#1\endcsname}%
  {\ERROR}%
}

或者,使用内核构造

\newcommand*{\foo}[1]{%
  \@ifundefined{foo@#1}{\ERROR}{\csname foo@#1\endcsname}%
}

答案2

这是 的一个“特性”,\csname以前未定义的命令被定义为\let通过\relax... 访问\csname\endcsname通常这是不受欢迎的行为,但它在 LaTeX 的几个地方使用,因为它是在经典 TeX 中在仅扩展上下文中进行定义的唯一方法,因此可用于标记某些事物。(例如,它用于捕获在 中写入文件时遇到的有问题的字符filecontents。)

答案3

完成一点点David Carlisle 的精彩回答,请注意,如果你\foo{c}在你的例子中使用了它,那么不是已经扩展为“无”,因为\relax实际上不是无。如果你尝试扩展\relax,你会得到,\relax因为它是不可扩展的。1可以\relax阻止宏扩展为无不会阻止的事情,例如:

  • TeX 在读取 <整数常量>、<八进制常量>、<十六进制常量> 或 <glue> (以 <dimen><stretch><shrink> 的形式) 时的扩展过程;
  • 当 TeX 正在寻找\noalign\omit位于对齐的开头(tabularalign...)或行的末尾时(在这种情况下,即使几个空格标记也不会停止搜索过程,但\relax会,这可以防止\rowcolor在某些情况下工作 - 参见这条信息例如);
  • 可能还有很多其他的事情。

因此,当 TeX 最终在其胃深处消化它时,该\relax命令不会做太多的事情;但在其他层面上,它可以产生非常明显的效果。

在您的示例中,您可以通过在使用之前执行两个扩展步骤来亲自看到 的扩展\foo@c\relax而不是空扩展\show:第一步将 扩展\foo(抓取{c})为\csname foo@c\endcsname,第二步将 扩展\csname(抓取foo@c\endcsname)为\foo@c

\documentclass{article}

\makeatletter
\newcommand*\newfoo[2]{%
  \expandafter\newcommand\csname foo@#1\endcsname{#2}%
}

\newcommand*\foo[1]{\csname foo@#1\endcsname}
\makeatother

\begin{document}
\expandafter\expandafter\expandafter\show\foo{c}
\end{document}

输出到终端:

> \foo@c=\relax.
<recently read> \foo@c 

l.12 ...dafter\expandafter\expandafter\show\foo{c}

当然,下面的方法同样有效:

\foo{c}%
\makeatletter
\show\foo@c
\makeatother

但执行的副作用是\relax(!),而且没那么有趣……


脚注

  1. 然而,请注意,如果\relax定义为\def\relax{\relax},它的行为将完全不同 -\relax不是一个夸克

  2. 它仍然可以防止连字:f\relax f\relax i与比较ffi

相关内容