我正在阅读"权威Antlr 4参考",并了解与听众和访客的工作方式有关的想法。这本书特别好地解释了侦听器与SAX解析器的关系,并清楚地说明了在实现每个方法期间何时调用方法。我还可以看到侦听器非常适合将输入转换为输出,但我希望提供有关何时使用侦听器和何时使用访问者的简短解释/示例(或者在某些情况下应该同时使用它们?
我的特殊意图是创建一个解释器(Cucumber风格/TinyBasic Interpreter,带有一些自定义调用),它将检查语法错误并停止执行自定义函数的错误而不恢复 - 希望看到在antlr中完整实现这样的事情 - 如果有人碰巧知道的话。
提前感谢您的任何建议。
以下是我认为相关的书中的引述:
侦听器和访客机制之间的最大区别在于侦听器方法由 ANTLR 提供的 walker 对象调用,而访客方法必须使用显式访问调用遍历其子级。忘记在节点的子节点上调用 visit() 意味着这些子树不会被访问。
在访客模式中,您有能力指导树木行走,而在听众中,您只对树木行走者做出反应。
如果您打算直接使用解析器输出进行解释,访问者是一个不错的选择。您可以完全控制遍历,因此在条件语句中只访问一个分支,可以访问循环 n 次,依此类推。
如果将输入转换为较低级别(例如虚拟机指令),则这两种模式都可能有用。
你可以看看"语言实现模式",它涵盖了基本的解释器实现。
我主要使用访客模式,因为它更灵活。
这两种模式之间还有另一个重要区别:访问者使用调用堆栈来管理树遍历,而侦听器使用在堆上分配的显式堆栈,由步行者管理。这意味着对访问者的大输入可能会吹出堆栈,而侦听器不会遇到麻烦。
如果您的输入可能是无限的,或者您可能在调用树中非常深入地调用访问者,则应使用侦听器而不是访问者,或者至少验证解析树不太深。出于这个原因,一些公司的编码实践不鼓励甚至完全禁止非尾递归。
摘自书中,第 120 页。
如果我们需要特定于应用程序的返回值,访问者可以很好地工作,因为我们可以使用内置的 Java 返回值机制。如果我们不希望显式调用访问者方法来访问孩子,我们可以切换到侦听器机制。不幸的是,这意味着放弃使用 Java 方法返回值的清洁度。
这就是我使用访客的原因。
我在Golang
中使用基准测试进行了测试。侦听器比访客略快,但没有明显的优势。
cpu: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz
BenchmarkCalcVisitorCases
BenchmarkCalcVisitorCases-16 5342 224675 ns/op
BenchmarkCalcListenerCases
BenchmarkCalcListenerCases-16 4676 230239 ns/op
BenchmarkCalculatorVisitorCases
BenchmarkCalculatorVisitorCases-16 2678 428312 ns/op
BenchmarkCalculatorListenerCases
BenchmarkCalculatorListenerCases-16 2149 540031 ns/op