在事件方法之间传递数据

代码来自《Antlr权威指南》

基于Antlr实现一个简单计算器。

下面是语法文件LExpr.g4

复制代码
grammar LExpr;
@header{package com.example.listeners.expr;}
s : e ;

e : e MULT e         # Mult
  | e ADD e         # Add
  | INT                # Int
  ;

MULT: '*' ;
ADD : '+' ;
INT : [0-9]+ ;
WS : [ \t\n]+ -> skip ;
复制代码

我们的计算器很简单,只支持正整数的加法和乘法。

我们可以通过Antlr自动进行词法分析,并生成抽象语法树。

接下来我们计算每个表达式的值,并累计得到最终值。

我们可以通过三种不同的方式在事件方法之间共享数据。

一、监听器模式+栈

通过阅读下面的代码可以看出,我们通过监听器模式,计算每一个子表达式的值,并将它存入Stack,

父表达式从栈中弹出两个元素,求和或者求积,并将结果继续入栈。

表达式计算完毕,栈顶元素就是我们要得到的结果。

复制代码
public class TestLEvaluator {
    /** 基于栈机的计算器,通过stack缓存每一步的结果 */
    public static class Evaluator extends LExprBaseListener {
        Stack<Integer> stack = new Stack<Integer>();

        public void exitMult(LExprParser.MultContext ctx) {
            int right = stack.pop();
            int left = stack.pop();
            stack.push( left * right );
        }

        public void exitAdd(LExprParser.AddContext ctx) {
            int right = stack.pop();
            int left = stack.pop();
            stack.push(left + right);
        }

        public void exitInt(LExprParser.IntContext ctx) {
            stack.push( Integer.valueOf(ctx.INT().getText()) );
        }
    }

    public static void main(String[] args) throws Exception {
        String sdl = "1+2*3";
        CodePointCharStream input = CharStreams.fromString(sdl);
        LExprLexer lexer = new LExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LExprParser parser = new LExprParser(tokens);
        parser.setBuildParseTree(true);    
        ParseTree tree = parser.s(); 
    
        System.out.println(tree.toStringTree(parser));

        ParseTreeWalker walker = new ParseTreeWalker();
        Evaluator eval = new Evaluator();
        walker.walk(eval, tree);
        System.out.println("stack result = "+eval.stack.pop());
    }
}
复制代码

二、访问者模式+返回值

监听者模式没有返回值,所有只能用Stack进行事件方法之间的数据共享。

访问者模式有返回值,我们将自定义的Visitor的泛型参数设为Integer,就可以将表达式的结果return到上层调用者。

复制代码
public class TestLEvalVisitor {
    /** 基于访问者模式的计算器 */
    public static class EvalVisitor extends LExprBaseVisitor<Integer> {
        public Integer visitMult(LExprParser.MultContext ctx) {
            return visit(ctx.e(0)) * visit(ctx.e(1));
        }

        public Integer visitAdd(LExprParser.AddContext ctx) {
            return visit(ctx.e(0)) + visit(ctx.e(1));
        }

        public Integer visitInt(LExprParser.IntContext ctx) {
            return Integer.valueOf(ctx.INT().getText());
        }
    }

    public static void main(String[] args) throws Exception {
        String sdl = "1+2*3";
        CodePointCharStream input = CharStreams.fromString(sdl);
        LExprLexer lexer = new LExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LExprParser parser = new LExprParser(tokens);
        parser.setBuildParseTree(true);      
        ParseTree tree = parser.s(); 
        System.out.println(tree.toStringTree(parser));

        EvalVisitor evalVisitor = new EvalVisitor();
        int result = evalVisitor.visit(tree);
        System.out.println("visitor result = "+result);
    }
}
复制代码

三、监听者模式+树标记

我们再一次使用了监听者模式,不过没有使用Stack,而是选择了一个原生的ParseTreeProperty类存储每个节点对应的结果。

复制代码
public class TestLEvaluatorWithProps {
    public static class EvaluatorWithProps extends LExprBaseListener {
        /** 标注语法树,建立语法树节点ctx与节点属性的val关系 */
        ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();

        /** Need to pass e's value out of rule s : e ; */
        public void exitS(LExprParser.SContext ctx) {
            setValue(ctx, getValue(ctx.e())); // like: int s() { return e(); }
        }

        public void exitMult(LExprParser.MultContext ctx) {
            int left = getValue(ctx.e(0));  // e '*' e   # Mult
            int right = getValue(ctx.e(1));
            setValue(ctx, left * right);
        }

        public void exitAdd(LExprParser.AddContext ctx) {
            int left = getValue(ctx.e(0)); // e '+' e   # Add
            int right = getValue(ctx.e(1));
            setValue(ctx, left + right);
        }

        public void exitInt(LExprParser.IntContext ctx) {
            String intText = ctx.INT().getText(); // INT   # Int
            setValue(ctx, Integer.valueOf(intText));
        }

        public void setValue(ParseTree node, int value) { values.put(node, value); }
        public int getValue(ParseTree node) { return values.get(node); }
    }

    public static void main(String[] args) throws Exception {
        String sdl = "1+2*3";
        CodePointCharStream input = CharStreams.fromString(sdl);
        LExprLexer lexer = new LExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LExprParser parser = new LExprParser(tokens);
        parser.setBuildParseTree(true);     
        ParseTree tree = parser.s(); 
      
        System.out.println(tree.toStringTree(parser));

        ParseTreeWalker walker = new ParseTreeWalker();
        EvaluatorWithProps evalProp = new EvaluatorWithProps();
        walker.walk(evalProp, tree);
        System.out.println("properties result = " +evalProp.getValue(tree));
    }
}
复制代码

三种模式都可以得到正确的结果。

四、三种模式的对比

下面我们对这三种不同的数据共享方法做一个对比

为了获取可以复用的语法,我们需要使语法文件生成的抽象语法树和用户的业务逻辑代码进行分离。

这也意味着将程序的逻辑代码放在语法树之外的监听者或访问者中。

但是由于事件方法的签名是固定的,监听者、访问者都无法改变方法的参数和返回值。

所以我们需要使用上面的三种方法在事件方法之间共享数据。

1.使用一个栈Stack,模拟方法和返回值的入栈出栈

2.指定访问者返回的类型,但所有的方法也就都必须统一使用一种类型。

3.树标注,上下文维护一个Map,树节点为key,节点对应的结果为value。

这三种方案都能将程序的具体逻辑封装在特定对象内,从而使其与语法树本身完全解耦。

访问者模式的代码比较好理解,因为他们之间调用子节点的访问者获取结果,

缺点在于父节点必须显式访问子节点,而且每个访问者都必须返回同一个类型。

基于栈的共享方式通过栈来模拟参数和返回值,空间利用效率也很高,但在手工操作栈时存在失误的可能。

树标注是我个人的首选方案,它允许我向树节点传递任意信息来操作树的任意节点,key传递任意类型的参数值,

在多个方法中任意传递数据也不容易失误,缺点是需要保存大量数据,比较占内存。

某些需求比如建立符号表、引用消解等,因为需要多次遍历语法分析树。

后面的遍历需要基于前几次遍历的结果,需要使用树标注来实现数据共享。

 

posted @   Mars.wang  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示