Java 是跳绳吗;接受;在已知类型的情况下,是否对访问者模式进行了有效的优化?
考虑下面的访问者作为一个简单的语言解释器Java 是跳绳吗;接受;在已知类型的情况下,是否对访问者模式进行了有效的优化?,java,visitor,code-design,Java,Visitor,Code Design,考虑下面的访问者作为一个简单的语言解释器 public interface Visitor{ void visit( VarStat vs); void visit( Ident i); void visit( IntLiteral a); void visit( Sum s); } 为了完整起见,我添加了一些代码,提供了必要的实现细节(您可以跳过并直接阅读问题) var语句的定义如下: VarStat ::== var Ident = Exp; Exp ::=
public interface Visitor{
void visit( VarStat vs);
void visit( Ident i);
void visit( IntLiteral a);
void visit( Sum s);
}
为了完整起见,我添加了一些代码,提供了必要的实现细节(您可以跳过并直接阅读问题)
var语句的定义如下:
VarStat ::== var Ident = Exp;
Exp ::== Exp + Exp | IntLiteral | Ident
IntLiteral ::== [0-9]{0,8}
Ident ::== [a-zA-Z]+
有效的语言实例
var x = x+y+4;
表示VarStat
节点的抽象方法如下:
. _____VarStat _____
. / / | \ \
. / / | \ \
. / / | \ \
. "var" Ident "=" Exp ";"
问题 通常的VisitorPattern应用程序是
void visit( VarStat vs){
vs.getIdent().accept( this);
vs.getExp().accept( this);
//...
}
然而,由于我知道“Ident”属于Ident
类型,因此可能需要进行优化
void visit( VarStat vs){
visit( vs.getIdent());
vs.getExp().accept( this);
//...
}
这将跳过2个提高性能的方法调用(实际上,在我的场景中,它提供了一个很好的提升)
这是否被视为可能导致未来问题的设计错误?是否会导致问题?很难说。(我想如果语法改变的话,可能会让人感到惊讶……) 但我认为真正的问题是,这是否是一个有价值的优化。具体来说,保存两个方法调用是否会在整体上产生显著的差异?我的直觉是不会的 这个解释器的性能真的重要吗
如果是,为什么在解释器中使用访问者模式?您不应该编译为中间虚拟机代码吗?或者字节码?在这种情况下,它不是访问者模式。如果它符合您的需求,则不必如此,访问者经常被误用并导致过度架构
然而,你会失去潜在的好处。例如,在将调用转发到装饰/代理对象之前,您将无法为Ident创建装饰器或代理,也无法在accept方法中执行其他操作。Visitor只是一个复杂的脚手架,用于在Java等语言上实现双重分派 处理叶类型时,不需要双重分派;运行时类型在编译时已知。直接分派一个叶类型不仅是一种优化,而且更不合原则 当然,问题是,在未来,叶类型可能会变成超级类型。对于当今IDE中的重构工具,这不是一个大问题 与其为未知的未来需求进行复杂的设计,不如为当前的需求进行简单的设计
在Java8中,我们可以使用非常接近真实的双重分派的语法实现双重分派
final DoubleDispatch<Root,Void> dd = new DoubleDispatch<>();
dd.register(X.class, x->
{
do something with x; its compile time type is X
return null;
});
dd.register(Y.class, y->
{
do something with y; its compile time type is Y
return null;
});
// etc
...
dd.invoke( something );
// ----
public class DoubleDispatch<T, R>
{
public R invoke(T obj){...}
public <C extends T> void register(Class<C> type, Function<C,R> func){...}
}
final DoubleDispatch dd=new DoubleDispatch();
dd.register(X.class,X->
{
对x做些什么;它的编译时类型是x
返回null;
});
dd.寄存器(Y.类,Y->
{
用y做点什么;它的编译时类型是y
返回null;
});
//等
...
调用(某物);
// ----
公共类双重分派
{
公共R调用(T obj){…}
公共无效寄存器(类类型,函数func){…}
}
另请参见-实际上,它在我的场景中提供了一个很好的提升。真正地怎么做?是的,我也想看看测量的数字。快15%。在20个地方使用了超过25种方法(真正的访问者现在有25种方法)。由于堆栈跟踪变得更小,调试也更容易,为什么时间对您很重要?相关-没有访问者模式的双重分派-@Jayan-这是合理的。新代码更容易针对JVM进行优化;它可能是内联的。
final DoubleDispatch<Root,Void> dd = new DoubleDispatch<>();
dd.register(X.class, x->
{
do something with x; its compile time type is X
return null;
});
dd.register(Y.class, y->
{
do something with y; its compile time type is Y
return null;
});
// etc
...
dd.invoke( something );
// ----
public class DoubleDispatch<T, R>
{
public R invoke(T obj){...}
public <C extends T> void register(Class<C> type, Function<C,R> func){...}
}