第 41 页底部TeXbook,有一个疑问:
定义一个
\appendroman
具有三个参数的控制序列,使得\appendroman#1#2#3
定义控制序列#1
扩展为一个控制序列,其名称是控制序列的名称#2
后跟用罗马数字表示的正整数的值#3
。例如,假设\count20
等于 30;那么\appendroman\a\TeX{\count20}
应该具有与 相同的效果\def\a{\TeXxxx}
。
看到这里,我有了自己的答案,虽然我知道这个答案是不对的,那就是\def\appendroamn#1#2#3{\def#1{\#2#3}}
。那本书的正确答案是
\def\gobble#1{} % remove one token
\def\appendroman#1#2#3{\expandafter\def\expandafter#1\expandafter
{\csname\expandafter\gobble\string#2\romannumeral#3\endcsname}}
有这么多\expandafter
s,这让我很困惑。我知道第一行发出一个命令\gobble
,该命令定义了\gobble{ }
什么都不产生。但是这里的第二行和第三行该如何理解?
答案1
总结我最喜欢读的是\expandafter
:教程\expandafter
(拖船 9)
让我们看一下示例:代码
\appendroman\a\TeX{\count20}
扩展为
\expandafter\def\expandafter\a\expandafter
{\csname\expandafter\gobble\string\TeX\romannumeral\count20\endcsname}
现在,第一次\expandafter
保存\def
以供以后使用:我通过将标记放在括号中来表示这一点:
(\def)\expandafter\a\expandafter
{\csname\expandafter\gobble\string\TeX\romannumeral\count20\endcsname}
现在还有进一步的\expandafter
,它再次保存下一个标记以供以后使用
(\def)(\a)\expandafter
{\csname\expandafter\gobble\string\TeX\romannumeral\count20\endcsname}
第三次同样的游戏,现在这一次支撑保存以供日后使用
(\def) (\a) ({) \csname\expandafter\gobble\string\TeX\romannumeral\count20\endcsname}
现在 TeX 会遇到并开始用和\csname
之间的所有内容构建一个控制序列名称(直接丢弃后面的空格)\csname
\endcsname
\csname
(直接丢弃但不是其他)。\csname...\endcsname
是这样的一切被扩展,直到只剩下字符标记。因此,控件名称源自
\expandafter\gobble\string\TeX\romannumeral\count20
游戏继续:\expandafter
保存\gobble
以备后用,并\string
返回\
12 T
12 e
12 X
12。但现在\gobble
开始:它删除以下标记,即反斜杠(实际上是\escapechar
),因此TeX
只剩下 (所有类别代码为 12 的)。扩展继续,并\romannumeral\count20
返回xxx
(因为\count20
假设等于 30)。因此,在\csname
和之间完全扩展后,\endcsname
我们现在有TeXxxx
,它构建了控制序列标记\TeXxxx
。但我们仍然有来自前一个 的保存的标记\expandafter
:
(\def) (\a) ({) \TeXxxx}
因此 TeX 现在返回并有效地找到
\def\a{\TeXxxx}
现在实际打印了什么?可能什么都没有。当您在文件中\a
写入时,它会扩展为,现在又出现了一个奇怪的现象。我引用(TeX 书中的练习 7.7):“\a
\TeXxxx
当\csname
首次使用 定义控制序列时,该控制序列将被视为等同于 ,\relax
直到重新定义为止。“这意味着
\appendroman\a\TeX{30}\a
没有打印任何内容,但
\def\TeXxxx{hello}
\appendroman\a\TeX{30}\a
您会在输出中发现“hello”。
答案2
下面列出了一系列步骤(重要:这些不是扩展步骤,因为它们是由 TeX 完成的,而是由 TeX 内部完成的更多处理步骤)。
需要注意的是,所有这些(除了最后一步,执行\def
)都是在单个扩展步骤中发生的。情况就是这样,因为\expandafter
会在一个步骤中扩展为紧随其后的第二个标记的扩展结果,从而\expandafter
从流中移除 。 也会\csname
在一个步骤中扩展,扩展所有可扩展的内容,直到它命中\endcsname
(或者如果它命中了在扩展后不是字符标记的内容,则会引发低级错误),将其结果留在流中。
所以按照你的定义
\def\gobble#1{} % remove one token
\def\appendroman#1#2#3%
{%
\expandafter\def
\expandafter#1%
\expandafter{%
\csname
\expandafter\gobble
\string#2\romannumeral#3%
\endcsname
}%
}
我们将看一下该宏的单一用法。在下面的格式中,第一行是总结当前步骤的注释,然后可能是延迟信息,然后是未注释的行,即 TeX 检查的输入流。
% step 1: simple expansion
\appendroman\a\TeX{30}
% step 2: defer \expandafter<TOKEN>
\expandafter\def
\expandafter\a
\expandafter{%
\csname
\expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 3: defer \expandafter<TOKEN>
% Deferred expandafter-chain: \expandafter\def
\expandafter\a
\expandafter{%
\csname
\expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 4: defer \expandafter<TOKEN>
% Deferred expandafter-chain: \expandafter\def\expandafter\a
\expandafter{%
\csname
\expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 5: start \csname-expansion
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
\csname
\expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 6: defer \expandafter<TOKEN>
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname
\expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 7: \string (turning the token \TeX into four tokens of category 12)
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname
% Deferred expandafter-chain: \expandafter\gobble
\string\TeX\romannumeral30%
\endcsname
}
% step 8: inner expandafter-chain done, put back
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname
% Deferred expandafter-chain: \expandafter\gobble
\TeX\romannumeral30%
\endcsname
}
% step 9: \gobble one token (category 12 \)
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname
\gobble
\TeX\romannumeral30%
\endcsname
}
% step 10: defer unexpandable tokens inside \csname
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname
TeX\romannumeral30%
\endcsname
}
% step 11: \romannumeral expansion, start scanning for a number
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
\romannumeral30%
\endcsname
}
% step 12: scanning number
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
% \romannumeral-expansion: \romannumeral
30%
\endcsname
}
% step 13: scanning number
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
% \romannumeral-expansion: \romannumeral 3
0%
\endcsname
}
% step 14: scanning number
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
% \romannumeral-expansion: \romannumeral 30
\endcsname
}
% step 15: \romannumeral hitting \endcsname, end of number put back result
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
% \romannumeral-expansion: \romannumeral 30
\endcsname
}
% step 16: defer unexpandable tokens inside \csname
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeX
xxx%
\endcsname
}
% step 17: \endcsname (stop \csname building, put back result)
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
% \csname-expansion: \csname TeXxxx
\endcsname
}
% step 18: expandafter-chain done, put back
% Deferred expandafter-chain: \expandafter\def\expandafter\a\expandafter{%
\TeXxxx
}
% step 19: expandafter-chain done, put back
% Deferred expandafter-chain: \expandafter\def\expandafter\a
{%
\TeXxxx
}
% step 20: expandafter-chain done, put back
% Deferred expandafter-chain: \expandafter\def
\a{%
\TeXxxx
}
% step 21: expandafter-chain done, put back
\def\a{%
\TeXxxx
}
% step 22: execute \def (we don't examine the collecting of \def's arguments here)
% assign meaning \a=macro:->\TeXxxx.
%% done
答案3
已经有了很好的解释。我想强调一下 Knuth 解决方案的一个可能弱点,它假设\escapechar
指向一个实际的字符。如果\escapechar
超出字符范围,解决方案将失败,对于 TeX 来说从 0 到 255(0x10FFFF
对于 Unicode 引擎则扩展到),因为\gobble
会从控制序列名称中吃掉一个字符。
这是一个没有问题的解决方案,因为\escapechar
在执行业务之前设置为负一,但是在一个组内部。
\def\appendroman#1#2#3{%
% #1 = cs to be defined
% #2 = cs to be appended a Roman numeral
% #3 = integer (implicit or explicit)
\begingroup\escapechar=-1 \expandafter\endgroup
\expandafter\def\expandafter#1\expandafter{%
\csname\string#2\romannumeral#3\endcsname
}%
}
\appendroman\a\TeX{30} \show\a
\count20=18
\appendroman\a\TeX{\count20} \show\a
\showthe\escapechar
在控制台上
> \a=macro:
->\TeXxxx .
l.11 \appendroman\a\TeX{30} \show\a
?
> \a=macro:
->\TeXxviii .
l.15 \appendroman\a\TeX{\count20} \show\a
?
> 92.
l.17 \showthe\escapechar
说明运作符合预期,且\escapechar
业务完成后价值得到恢复。
注意重要的后面有一个空格-1
。如果忘记了,则\escapechar
只有在链执行完其操作后才会对 进行赋值\expandafter
。但无论如何,常量后面的空格是良好做法。
诀窍在于,\expandafter
before\endgroup
开始扩展,最终触发 的扩展\csname
,后者会进行完全扩展,直到找到。在第一个示例中,在、和完成工作\endcsname
之后的输入流将是\expandafter
\csname
\rommannumeral
\endgroup\def\a{\TeXxxx}
剩下的部分将恢复集团成立时的原有\endgroup
价值。\escapechar
替代解决方案是使用单个\expandafter
,但使用\noexpand
两次:
\def\appendroman#1#2#3{%
% #1 = cs to be defined
% #2 = cs to be appended a Roman numeral
% #3 = integer (implicit or explicit)
\begingroup\escapechar=-1
\edef\x{\endgroup\def\noexpand#1{%
\expandafter\noexpand\csname\string#2\romannumeral#3\endcsname
}}\x
}
相同之处expl3
:
\ExplSyntaxOn
\cs_new_protected:Npn \appendroman #1 #2 #3
{
\cs_set:Npe #1 { \exp_not:c { \cs_to_str:N #2 \int_to_roman:n { #3 } } }
}
\ExplSyntaxOff