我有一个对象的层次结构来表示算术表达式。如下所示:
TreeNode
/ \
/ \
/ \
NumericNode BinaryOpNode
/ | \ \
/ | \ \
/ | \ \
AddNode MulNode DivNode ...etc
我有词法分析器和解析器工作。现在,我试图找出以实际且易于调整的方式对生成的AST执行操作的最佳方法。 现在我已经使用Visitor和Visitable接口实现了访问者模式,但我不确定我是否真的需要所有具体的类来实现Visitable接口。
由于我想控制访问者的遍历顺序,我发现自己重复了很多代码。例如,对于PrintInOrder Visitor,我有以下方法:
public void visit(AddNode node) {
node.getLeft().accept(this);
System.out.print(node + " ");
node.getRight().accept(this);
}
表示某些算术运算的每个具体节点。
但是我可以在超类中实现相同的Visitable实现。例如,相同的PrintInOrder访问者将如下所示:
public void visit(BinaryOpNode node) {
node.getLeft().accept(this);
System.out.print(node + " ");
node.getRight().accept(this);
}
但是,我不知道这项任务最常用的方法是什么。
问题1:如果我希望我的设计让我几乎可以做任何我想要的AST,我真的需要访问所有具体的节点并写下所有重复的代码吗?
问题2:假设访问者可以返回某种类型,例如:
public interface Visitor<T> {
public T visit(AddNode node);
public T visit(MulNode node);
...
}
这看起来比普通的无效版本更通用。但它有用吗?并且可以像void版本一样使用吗?
答案 0 :(得分:3)
答案1:您可以使用抽象超类访问方法和任何常见功能,这些功能在每个具体类中都是相同的,因此每个具体类都可以从此抽象类派生并使用该方法而不实现这是一种多余的方式。
正如您在图中所看到的,您可以在单独的类中实现访问者逻辑。这样您就可以解耦遍历顺序,以及从Node实现访问节点的实现。如果根据算术运算优先级构建了抽象语法树,则不必更改不同类型节点的实现。您可以为每个使用相同的ConcreteVisitor。
答案2:是的,使用泛型类在很多方面都是有益的。这样你可以做类型cheking编译时间。如果你没有这个机会,你可能有隐藏的bug,很难找到,但如果你可以检查类型有效性编译时间,你可以确保你的系统中没有这种类型的错误。
答案 1 :(得分:2)
到目前为止,我用来解析和解释简单算术表达式的最佳方法是构建一个Pratt解析器。下面的链接给出了一个很好的解释过程,这些例子甚至用java编写。
基本上,您最终会将单独的迷你解析器作为您的应用支持的每个运营商的功能。您的所有运营商都将被分类为前缀,中缀或后缀。此外,您还要为评估每个子表达式时引用的每个运算符指定一个优先级值。
我在C#中编写了一个ECMAScript解释器,它使用语言结构的递归后代解析器和评估控制流语句。并且,它使用Pratt解析器来计算表达式。 Pratt解析器为您提供了一种处理运算符关联性和优先级的方法。
我只使用了访问者模式来生成AST本身的漂亮打印和无空白格式的代码。其他一切都是使用继承和接口完成的。如果你真的想使用Visitor的想法,那么基于堆栈的表达式解析器就是你想要做的。 Pratt Parser只是使用你的应用程序的调用堆栈。
作为一名java程序员,我认为您会发现这篇文章特别有用。无论哪种方式。保重,祝你好运。
http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/