目录
介绍
复杂的解析器通常需要支持回溯(backtracking),这是一种重新访问您已经遇到的项的方法。这有两个诀窍,一是高效,二是透明。LookAheadEnumerator<T>类同时提供这两点。
更新: 错误修复,更强大的文档注释,并且90%俚语化的代码(被删除的switch/ nameof 等),以便俚语可以烹饪它,并且可以在生成的解析器代码中使用它。
背景
在解析器代码中,通常希望使用某种“流”接口作为其输入,如TextReader类或IEnumerator<T>的实现类。我喜欢枚举器,因为它们无处不在且简单。但是,在解析之前不将其预加载到内存中就很难回溯流式源。这适用于小文本,但不能用于大量JSON。
通常,使用枚举器,您只能做的事是MoveNext(),有时是Reset(),如果你是幸运的话。没有办法找回以前的特定位置,即使存在,它也可能无法用于真正的流媒体源,例如HTTP响应或控制台输入。
另一方面,回溯解析器需要“收藏”其当前位置,然后再尝试几种选择,直到找到要解析的解析器为止。这意味着多次重新访问相同的文本序列。
回溯解析器本质上效率较低,但比非回溯解析器灵活得多。我已经尽了最大的努力来优化此类,以使其达到尽可能高效的目的。
整理概念
我已经将数组支持的队列嵌入到此类中,该队列用于支持lookahead缓冲区。队列从16个元素开始,并根据需要增长(每次的大小几乎加倍,以避免过多的重新分配——.NET中的堆比CPU便宜),这取决于需要多少lookahead。当LookAheadEnumeratorEnumerator<T> (lookahead游标)前进时,通常需要主类将更多数据读入队列才能满足要求。当主光标前进时,它将丢弃队列中的项(简单地增加_queueHead,这确实非常快)。在使用lookahead光标时前进或重置主光标不是一个好主意。在这种情况下,结果是不确定的,因为我没有在这些枚举器中实现版本控制。
处理
您可以像使用标准IEnumerator<T>一样使用代码,并附加一个属性LookAhead,它允许您在不推进光标的情况下从当前位置前进。还有Peek()和TryPeek()可以向前看指定数量的位置,DiscardLookAhead()可以简单地将光标移动到物理位置并清除缓冲区。
var text = "fubarfoobaz";
var la = new LookAheadEnumerator<char>(text.GetEnumerator());
la.MoveNext(); // can't look ahead until we're already over the position
// we want to start look at.
foreach (var ch in la.LookAhead)
Console.Write(ch);
Console.WriteLine();
while (la.MoveNext())
{
foreach (var ch in la.LookAhead)
Console.Write(ch);
Console.WriteLine();
}
这会将以下内容打印到控制台:
fubarfoobaz
ubarfoobaz
barfoobaz
arfoobaz
rfoobaz
foobaz
oobaz
obaz
baz
az
z
如您所见,我们在每次迭代中将主光标增加一个,然后从那里开始枚举LookAhead。枚举LookAhead不会影响主光标。
底层的物理读取游标是高级的,这是必须的,但是facade是使用一个队列来显示的,该队列为您缓冲已经读取的输入,并将其显示为下一个输入。
通常,在解析器中,您将在令牌上使用它,例如LookAheadEnumerator<Token>,然后每次需要进行回溯时,都将解析LookAhead而不是主光标。当找到匹配项后,您将必须丢弃所有匹配的令牌,方法是沿着主光标重新解析,或者如果知道解析了多少令牌,则计数并前进。如果只解析主解析的一个替代项,那么只要匹配了该替代项,就可以简单地使用DiscardLookAhead()。