采用特定的方式解耦
语法树解析出来后,还需要进行动作执行,而执行动作的这个过程,则是Listener去干的
Listener和Visitor模式
原始语法
grammar PropertyFile;
file : {«start file»} prop+ {«finish file»} ;
prop : ID '=' STRING '\n' {«process property»} ;
ID : [a-z]+ ;
STRING : '"' .*? '"' ;
但是这样的重用性就很低,稍作修改
grammar PropertyFile;
@members {
void startFile() { } // blank implementations
void finishFile() { }
void defineProperty(Token name, Token value) { }
}
file : {startFile();} prop+ {finishFile();} ;
prop : ID '=' STRING '\n' {defineProperty($ID, $STRING)} ; ID : [a-z]+ ;
STRING : '"' .*? '"' ;
class PropertyFilePrinter extends PropertyFileParser
{
void defineProperty(Token name, Token value) {
System.out.println(name.getText()+"="+value.getText());
}
}
//所以在启动的时候,使用的类就要用我们自己的了
PropertyFileLexer lexer = new PropertyFileLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
PropertyFilePrinter parser = new PropertyFilePrinter(tokens);
parser.file(); // launch our special version of the parser
优化的第一种方法是侦听器方法,因为Listener是在Parse完成后才执行的,里面的转换动作都是自动执行的
public static class PropertyFileLoader extends PropertyFileBaseListener {
Map<String,String> props = new OrderedHashMap<String, String>();
public void exitProp(PropertyFileParser.PropContext ctx) {
String id = ctx.ID().getText(); // prop : ID '=' STRING '\n' ;
String value = ctx.STRING().getText();
props.put(id, value);
}
}
...
//然后就可以walk了
ByteArrayInputStream is = new ByteArrayInputStream("a=b\nc=d\n".getBytes());
ANTLRInputStream input = new ANTLRInputStream(is);
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
fileLexer lexer = new fileLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
fileParser fileParser = new fileParser(tokens);
SampleLoader sampleLoader = new SampleLoader();
parseTreeWalker.walk(sampleLoader, fileParser.file());
System.out.println(sampleLoader.props);
而且有个好处是,这里的实例是同一个
另外一种方式是访问者模式
//进行访问
ByteArrayInputStream is = new ByteArrayInputStream("a=b\nc=d\n".getBytes());
ANTLRInputStream input = new ANTLRInputStream(is);
fileLexer lexer = new fileLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
fileParser fileParser = new fileParser(tokens);
SampleVisitor sampleLoader = new SampleVisitor();
sampleLoader.visit(fileParser.file());
System.out.println(sampleLoader.props);
在语法中使用标签
例如有如下语法
grammar Expr;
s : e ;
e : e op=MULT e // MULT is '*'
| e op=ADD e // ADD is '+'
| INT
;
MULT: '*' ;
ADD : '+' ;
INT : [0-9]+ ;
WS : [ \t\n]+ -> skip ;
这个时候,操作起来就好麻烦了,因为exitE会被连续调用多次,以1+2为例,1会被调用一次,2会被调用一次 1+2完成后会被调用一次
public void exitE(ExprParser.EContext ctx) {
if ( ctx.getChildCount()==3 ) { // operations have 3 children
int left = values.get(ctx.e(0));
int right = values.get(ctx.e(1));
if ( ctx.op.getType()==ExprParser.MULT ) {
values.put(ctx, left * right);
}
else {
values.put(ctx, left + right);
}
}
else {
values.put(ctx, values.get(ctx.getChild(0))); // an INT
}
}
所以我们需要在3个参数都凑齐的时候才做操作,所以,ANTLR提供了一个方便的方法,可以标签化操作,把 语法修改如下
grammar LExpr;
s : e ;
e : e MULT e # Mult
| e ADD e # Add
| INT # Int
;
MULT: '*' ;
ADD : '+' ;
INT : [0-9]+ ;
WS : [ \t\n]+ -> skip ;
这个时候使用语法树查看器会看到语法生成树上面多了Mult和ADD这样的标签
public class ExprListener extends ExprBaseListener {
@Override
public void exitMult(ExprParser.MultContext ctx) {
super.exitMult(ctx);
}
@Override
public void exitAdd(ExprParser.AddContext ctx) {
super.exitAdd(ctx);
}
}
通过使用标签化语法,可以使得逻辑更加清晰
在事件中共享信息
采用Visitor模式
ByteArrayInputStream is = new ByteArrayInputStream("1+2*3".getBytes());
ANTLRInputStream input = new ANTLRInputStream(is);
ExprLexer lexer = new ExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser exprParser = new ExprParser(tokens);
ExprResultVisitor exprVisitor = new ExprResultVisitor();
int result = exprVisitor.visit(exprParser.s());
System.out.println(result);
visitor代码如下
public class ExprResultVisitor extends ExprBaseVisitor<Integer> {
@Override
public Integer visitAdd(ExprParser.AddContext ctx) {
return visit(ctx.e(0)) + visit(ctx.e(1));
}
@Override
public Integer visitInt(ExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
@Override
public Integer visitMult(ExprParser.MultContext ctx) {
return visit(ctx.e(0)) * visit(ctx.e(1));
}
}
每当触发visit的时候,就会根据情况往下调用一次,多层次的调用把结果回到顶层,然后就获取到最终的结果了,当我们walk完一次就要回结果的时候,这个方式挺适合的
使用堆栈来模拟值返回
Listener模式下是没有返回值的,这个时候我们可以使用栈的方式来模拟值的返回
ByteArrayInputStream is = new ByteArrayInputStream("1+2*3".getBytes());
ANTLRInputStream input = new ANTLRInputStream(is);
ExprLexer lexer = new ExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser exprParser = new ExprParser(tokens);
ExprListener exprListener = new ExprListener();
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
parseTreeWalker.walk(exprListener, exprParser.s());
System.out.println(exprListener.stack.pop());
Listener采用stack来做数据结构
public class ExprListener extends ExprBaseListener {
Stack<Integer> stack = new Stack<Integer>();
public void exitMult(ExprParser.MultContext ctx) {
int right = stack.pop();
int left = stack.pop();
stack.push(left * right);
}
public void exitAdd(ExprParser.AddContext ctx) {
int right = stack.pop();
int left = stack.pop();
stack.push(left + right);
}
public void exitInt(ExprParser.IntContext ctx) {
stack.push(Integer.valueOf(ctx.INT().getText()));
}
}
麻烦是麻烦了一些,不过Listener返回多个值这样的情况还是很直观的
解析树上增加变量
这种做法是在解析数运算的时候,每次运算完,都把自己这个节点算出来的放回自己的节点上,这样,下一个节点计算的时候,看到自己这个节点就已经是一个运算完的结果,直接计算就好了
ByteArrayInputStream is = new ByteArrayInputStream("1+2*3".getBytes());
ANTLRInputStream input = new ANTLRInputStream(is);
ExprLexer lexer = new ExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser exprParser = new ExprParser(tokens);
ExprListener exprListener = new ExprListener();
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
ExprParser.SContext tree = exprParser.s();
parseTreeWalker.walk(exprListener, tree);
System.out.println(exprListener.values.get(tree));
在上下文中添加结果
public class ExprListener extends ExprBaseListener {
ParseTreeProperty<Integer> values = new ParseTreeProperty<>();
public void exitS(ExprParser.SContext ctx) {
values.put(ctx, values.get(ctx.e()));
}
public void exitMult(ExprParser.MultContext ctx) {
int left = values.get(ctx.e(0));
int right = values.get(ctx.e(1));
values.put(ctx, left * right);
}
public void exitAdd(ExprParser.AddContext ctx) {
int left = values.get(ctx.e(0));
int right = values.get(ctx.e(1));
values.put(ctx, left + right);
}
public void exitInt(ExprParser.IntContext ctx) {
values.put(ctx, Integer.valueOf(ctx.INT().getText()));
}
}
假如要用HashMap的话,要用
IdentityHashMap
而不是普通的HashMap
模式 | 优缺点 |
---|---|
返回值模式 | 易读 |
模拟堆栈 | 容易出错,不过多返回值的时候易读 |
解析树标记 | 优先选择这种,比较好操作,也不容易出错 |