循环遍历字符串

循环遍历字符串

在玩字符串的过程中,我发现了三种不同的宏,可以逐个字符地循环遍历字符串。但是,我不太清楚它们的具体工作原理。有人能解释一下它们各自的机制吗?哪一个是最“正确”的?还有其他方法可以构造这个宏吗?

我已经改变了原来的版本,以便它们看起来尽可能的相似。

基于 Tarass 的宏:

对我来说,这是最容易理解的。由于参数是分隔的,因此第一个参数是第一个字母,其余的参数将重新引入循环中。循环在语句内进行\if

基于 David Carlisle 的宏:

这个比较棘手。循环来自\if语句内部,但实际上并非如此。如果\expandafter消除了,它就不起作用。如果\xloop将放在后面,\fi它也不会起作用。那么究竟发生了什么?

其余的部分就简单多了,每次\xloop执行时,它都会找到字符串的剩余部分并取第一个字符。

基于 Florent 的宏:

这个还是比较复杂的(至少对我来说)。循环在语句外部进行\if,但字符像以前一样逐个访问。令人惊讶的是,这里没有丢弃空格。这也可以在前面的宏中使用来实现\obeyspaces

\documentclass{article}

\begin{document}

\subsection*{Macro based on one by Tarass:}

\def\xloop<#1#2>{%
  \ifx\relax#1
    \else
      (#1)\xloop<#2>%
  \fi}  
\def\markletters#1{\xloop<#1\relax>}

\markletters{Hello World!}

\subsection*{Macro based on one by David Carlisle:}

\def\xloop#1{%
  \ifx\relax#1
    \else
      (#1)\expandafter\xloop%
  \fi}
\def\markletters#1{\xloop#1\relax}%

\markletters{Hello World!}


\subsection*{Macro based on one by Florent:}

\def\gobblechar{\let\xchar= }
\def\assignthencheck{\afterassignment\xloop\gobblechar}
\def\xloop{%
  \ifx\relax\xchar
      \let\next=\relax
    \else
      (\xchar)\let\next=\assignthencheck
  \fi
  \next}
\def\markletters#1{\assignthencheck#1\relax}

\markletters{Hello World!}


\end{document}

在此处输入图片描述

答案1

如果我稍微修改一下你的测试,让追踪论证更简短

\documentclass{article}

\def\test#1{{
\tracingonline=1
\tracingmacros=1
\markletters{#1}
}
\typeout{TYPEOUT: \markletters{#1}}
}
\begin{document}

\subsection*{Macro based on one by Tarass:}

\def\xloop<#1#2>{%
  \ifx\relax#1
    \else
      (#1)\xloop<#2>%
  \fi}  
\def\markletters#1{\xloop<#1\relax>}

\test{a bc}

\subsection*{Macro based on one by David Carlisle:}

\def\xloop#1{%
  \ifx\relax#1
    \else
      (#1)\expandafter\xloop%
  \fi}
\def\markletters#1{\xloop#1\relax}%

\test{a bc}


\subsection*{Macro based on one by Florent:}

\def\gobblechar{\let\xchar= }
\def\assignthencheck{\afterassignment\xloop\gobblechar}
\def\xloop{%
  \ifx\relax\xchar
      \let\next=\relax
    \else
      (\xchar)\let\next=\assignthencheck
  \fi
  \next}
\def\markletters#1{\assignthencheck#1\relax}

\test{a bc}


\end{document}

那么第一个测试产生

\markletters #1->\xloop <#1\relax >
#1<-a bc

\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi 
#1<-a
#2<- bc\relax 

\@nobreakfalse ->\global \let \if@nobreak \iffalse 

\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi 
#1<-b
#2<-c\relax 

\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi 
#1<-c
#2<-\relax 

\xloop <#1#2>->\ifx \relax #1 \else (#1)\xloop <#2>\fi 
#1<-\relax 
#2<-
TYPEOUT: (a)(b)(c) 

在这里您可以看到宏使用了一个分隔参数,因此每次都会抓取整个列表(所有标记直到>),并且处理第一个标记,其余的标记会在递归调用中重新插入。

  1. 因为#1它像普通的非分隔参数一样工作,所以它总是会删除空格。
  2. 由于整个事情都是通过扩展来实现的,因此它只在扩展环境中起作用,\write例如TYPEOUT: (a)(b)(c)
  3. \fi在每个阶段循环后都会插入一个,因此如果您有 1000 个条目,那么就会有 1000 个这样的条目,并且在某些时候您将过度填充输入堆栈。

第二个区块产生

\markletters #1->\xloop #1\relax 
#1<-a bc

\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi 
#1<-a

\@nobreakfalse ->\global \let \if@nobreak \iffalse 

\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi 
#1<-b

\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi 
#1<-c

\xloop #1->\ifx \relax #1 \else (#1)\expandafter \xloop \fi 
#1<-\relax 
TYPEOUT: (a)(b)(c) 

在这里你可以看到,在第一个宏之后,内部宏不会抓取整个列表,而只是抓取第一个标记。这样做可以避免重新解析列表,并使输入堆栈过载,但你需要在\fi执行递归调用之前扩展(为零),因为你无法像\fi第一个版本那样将放在列表之后。因此,\expandafter这迫使\fi在之前扩展\xloop

第三个版本

\markletters #1->\assignthencheck #1\relax 
#1<-a bc

\assignthencheck ->\afterassignment \xloop \gobblechar 

\gobblechar ->\let \xchar = 

\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next 

\@nobreakfalse ->\global \let \if@nobreak \iffalse 

\next ->\afterassignment \xloop \gobblechar 

\gobblechar ->\let \xchar = 

\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next 

\next ->\afterassignment \xloop \gobblechar 

\gobblechar ->\let \xchar = 

\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next 

\next ->\afterassignment \xloop \gobblechar 

\gobblechar ->\let \xchar = 

\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next 

\next ->\afterassignment \xloop \gobblechar 

\gobblechar ->\let \xchar = 

\xloop ->\ifx \relax \xchar \let \next =\relax \else (\xchar )\let \next =\assi
gnthencheck \fi \next 
! Undefined control sequence.

在这里,该项目是通过\let分配来抓取的,这具有看到空间标记的优势,但由于它不能通过扩展来工作,因此它在 中失败\typeout

答案2

第一种解决方案与另外两种解决方案有两个不同之处:与第一种解决方案相比,第二种和第三种解决方案都使用尾部递归。与此相关的是,第一种解决方案必须将参数从迭代复制到迭代,而其他两种解决方案只传递参数一次,逐个字符地处理而不复制其余部分。

第一个解决方案。在传统的编程语言中,if-then-else-fi 会被完全读取,然后才会执行其中一个分支的代码。在 TeX 中,\fi直到展开之前,它都会保留在输入中。因此,第一个循环会产生类似

\ifx\relax H\else(H)\xloop<ello...>\fi
(H)\ifx\relax e\else(e)\xloop<llo...>\fi\fi
(H)(e)\ifx\relax l\else(l)\xloop<lo...>\fi\fi\fi

注意\fi末尾的 s 是如何堆积的。

第二种解决方案。这是最紧凑的一个。\expandafter更改了扩展顺序:\fi首先扩展,因此\ifx在继续下一个之前先完成扩展\xloop

\xloop Hello...\relax
\ifx\relax H\else (H)\expandafter\xloop\fi ello...\relax
(H)\expandafter\xloop\fi ello...\relax
(H)\xloop ello...\relax
(H)\ifx\relax e\else (e)\expandafter\xloop\fi llo...\relax
(H)(e)\expandafter\xloop\fi llo...\relax
(H)(e)\xloop llo...\relax

第三种解决方案。 \afterassignment首先在测试宏之前将下一个字符“赋值”给宏。尾部递归是通过让\next指向结束之后要执行的代码来实现的\fi

\assignthencheck Hello...\relax
\xloop ello...\relax % \xchar is set to H
\ifx\relax\xchar\let\next=\relax\else(\xchar)\let\next=\assignthencheck\fi\next ello...\relax
(\xchar)\let\next=\assignthencheck\fi\next ello...\relax
(H)\next ello...\relax % \next points to the code of \assignthencheck
(H)\xloop llo...\relax % \xchar is set to e
(H)\ifx\relax\xchar\let\next=\relax\else(\xchar)\let\next=\assignthencheck\fi\next llo...\relax
(H)(\xchar)\let\next=\assignthencheck\fi\next llo...\relax
(H)(e)\next llo...\relax % \next points to the code of \assignthencheck

编辑:为了说明 David 和 Enrico 所说的前两个解决方案完全可扩展,而第三个解决方案不可扩展,请尝试

\edef\resultexpanded{\markletters{Hello World!}}

\edef扩展第二个参数,直到遇到不可扩展的 TeX 基元(或字符),然后使用结果定义\resultexpanded。对于 的前两个版本markletters\resultexpanded定义为

(H)(e)(l)(l)(o)(W)(o)(r)(l)(d)(!)

而在第三种情况下,它包含

\afterassignment\let\next=\relax\next\let\xchar=Hello World!\relax

这是一种不可扩展的休息。

答案3

Tarass 和 David Carlisle 的解决方案并没有什么不同。后者效率更高,因为它在进入下一个循环之前删除了条件,而前者将它们嵌套起来,并且在每个循环中都会将一个\fi添加到堆栈中:(非常)长的字符串可能会导致内存溢出。

另一个区别,即分隔符<>,是不相关的,因为它们根本不起任何作用。

忽略空格是宏可扩展性的代价:这意味着结果是直接通过宏扩展“计算”的。

第三种解决方案使用不同的方法:不是将下一个项作为宏的参数吸收(这是造成空间吞噬的原因),而是将控制序列交给下一个项,然后将其删除,直到到达\relax

请注意,您不能使用它来存储已处理的令牌列表,因为所有循环都用于\xchar打印该项目。

xparse和 的进一步解决方案expl3

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\markletters}{om}
 {
  \IfNoValueTF{#1}
   {
    \kessels_markletters:nn { #2 } { \tl_use:N \l_kessels_marked_letters_tl }
   }
   {
    \kessels_markletters:nn { #2 } { \tl_set_eq:NN #1 \l_kessels_marked_letters_tl }
   }
 }

\tl_new:N \l_kessels_unmarked_letters_tl
\tl_new:N \l_kessels_marked_letters_tl

\cs_new_protected:Nn \kessels_markletters:nn
 {
  \tl_set:Nn \l_kessels_unmarked_letters_tl { #1 }
  \tl_replace_all:Nnn \l_kessels_unmarked_letters_tl { ~ } { \textvisiblespace }
  \tl_clear:N \l_kessels_marked_letters_tl
  \tl_map_inline:Nn \l_kessels_unmarked_letters_tl
   {
    \tl_put_right:Nn \l_kessels_marked_letters_tl { (##1) }
   }
  #2
 }
\ExplSyntaxOff

\begin{document}

\markletters{Hello World!}

\markletters[\foo]{Hello World!}

\texttt{\meaning\foo}

\end{document}

空格首先被替换为(如果您愿意,\textvisiblespace也可以使用)。\nonbreakingspace

如果缺少可选参数,则打印处理后的字符串,否则将其存储在作为参数给出的宏中(不检查它是否未定义,但可以轻松添加)。

在此处输入图片描述

这是一个简化版本,仅用于打印结果。如您所见,代码要短得多,并且每个命令都执行了明确规定的任务。它与 Florent 的代码完全相同。

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\markletters}{m}
 {
  % save the input in a variable
  \tl_set:Nn \l_kessels_unmarked_letters_tl { #1 }
  % replace spaces with \textvisiblespace
  \tl_replace_all:Nnn \l_kessels_unmarked_letters_tl { ~ } { \textvisiblespace }
  % map the input surrounding each item by parentheses
  \tl_map_inline:Nn \l_kessels_unmarked_letters_tl { (##1) }
 }
\tl_new:N \l_kessels_unmarked_letters_tl
\ExplSyntaxOff

\begin{document}

\markletters{Hello World!}

\end{document}

相关内容