基于Jacc实现计算器AST与递归访问器的疑问:构建与类型转换
关于Jacc计算器AST与访问者模式的优化建议
嘿,我来帮你梳理下这个问题~首先得说:你既没有错误构建AST,也没有完全误解访问者模式,但当前的写法确实没发挥出访问者模式的核心优势,才导致了强制转换的繁琐感。下面具体拆解:
1. 访问者模式的正确打开方式:避免强制转换的关键
你现在遇到的强制转换问题,本质是没把访问者模式的“双分派”特性用起来。正确的做法应该是让AST节点主动接纳访问者,而不是让访问者去猜节点类型。
调整Expr接口与Visitor接口
首先给你的Expr接口添加一个accept方法,参数是访问者接口:
public interface Expr { void accept(Visitor visitor); }
然后定义Visitor接口,为每个具体的AST节点类对应一个visit方法:
public interface Visitor { void visitDoubleLiteral(DoubleLiteral literal); void visitAddExpr(AddExpr addExpr); void visitParenExpr(ParenExpr parenExpr); // 对应带括号的表达式 }
让具体节点类实现accept方法
每个AST节点类(比如DoubleLiteral、AddExpr、ParenExpr)实现accept方法,主动把自己传给访问者的对应方法:
public class DoubleLiteral implements Expr { private final double value; public DoubleLiteral(double value) { this.value = value; } public double getValue() { return value; } @Override public void accept(Visitor visitor) { visitor.visitDoubleLiteral(this); } }
实现DotVisitor
这样你的DotVisitor实现Visitor接口时,每个visit方法直接拿到的就是具体类型的节点,完全不需要强制转换:
public class DotVisitor implements Visitor { private final StringBuilder dotContent = new StringBuilder(); private int nodeId = 0; @Override public void visitDoubleLiteral(DoubleLiteral literal) { int id = nodeId++; dotContent.append(String.format("node%d [label=\"%f\"];\n", id, literal.getValue())); // 直接使用literal的属性,无需强制转换 } @Override public void visitAddExpr(AddExpr addExpr) { int id = nodeId++; dotContent.append(String.format("node%d [label=\"+\"];\n", id)); // 递归访问左右子节点 addExpr.getLeft().accept(this); addExpr.getRight().accept(this); // 添加节点间的边 dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-2)); dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-1)); } @Override public void visitParenExpr(ParenExpr parenExpr) { int id = nodeId++; dotContent.append(String.format("node%d [label=\"()\"];\n", id)); parenExpr.getInnerExpr().accept(this); dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-1)); } // 其他辅助方法... }
2. AST结构的小优化:拆分带括号的表达式
你提到Value产生式对应DoubleLiteral或带括号的AddExpr,建议把带括号的表达式单独抽成ParenExpr类,而不是让Value直接关联AddExpr。这样做的好处是:
- AST节点职责更单一:
ParenExpr专门处理括号包裹的逻辑,AddExpr专注于加法运算,DoubleLiteral就是纯数值节点 - 后续求值逻辑更清晰:遇到
ParenExpr时,直接递归求值其内部的Expr即可,不用额外判断 - 访问者模式的实现更规整,每个节点对应一个
visit方法,避免逻辑混杂
3. 关于左递归语法的补充
因为Jacc无法使用优先级指令,你采用左递归语法是完全正确的——左递归语法天然能生成左结合的AST,这符合加减运算的左结合规则(比如1+2+3会生成AddExpr(AddExpr(1,2),3)),后续求值时从左到右计算也符合预期,这部分没有问题。
内容的提问来源于stack exchange,提问作者Anonimista
相关产品推荐
相关产品推荐

