访问者模式中的accept()方法是什么意思? [英] What is the point of accept() method in Visitor pattern?

查看:266
本文介绍了访问者模式中的accept()方法是什么意思?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有很多关于将算法与类分离的讨论。但是,有一件事情没有解释。



他们使用这样的访客

 抽象类Expr {
public< T> T accept(访问者>访问者){visitor.visit(this);}
}

class ExprVisitor extends Visitor {
public Integer visit(Num num){
return num.value;
}

public整数访问(Sum sum){
return sum.getLeft()。accept(this)+ sum.getRight()。accept(this);
}

public整型访问(Prod prod){
return prod.getLeft()。accept(this)* prod.getRight()。accept(this);
}

访问者不要直接调用visit(element),要求元素调用它访问方式。它违反了对游客不了解类的声明。



PS1请用您自己的话说明或指出具体的解释。因为我有两个答复是指一般而不确定的东西。



PS2我的猜测:由于 getLeft()返回基本的表达式,调用访问(getLeft())将导致访问(Expression),而 getLeft()调用访问(this)将导致另一个更合适的访问调用。所以, accept()执行类型转换(aka cast)。



PS3 Scala的模式匹配=类固醇的访问者模式显示访问者模式多简单没有接受方法。 维基百科添加到本声明:通过链接显示 accept()方法在反射可用时是不必要的;为技术引入术语Walkabout。

解决方案

访问者模式的访问 / 接受由于C语言(C#,Java等)语义,构造是一个必要的邪恶。访客模式的目标是使用双重调度来路由您的呼叫,就像您期望阅读代码一样。



通常,当使用访问者模式时,涉及到一个对象层次结构,其中所有节点都从基础派生 Node 类型,以后称为 Node 。本能地,我们将这样写:

  Node root = GetTreeRoot(); 
new MyVisitor()。visit(root);

这里存在的问题。如果我们的 MyVisitor 类被定义如下:

  class MyVisitor implements IVisitor {
void visit(CarNode node);
无效访问(TrainNode节点);
void visit(PlaneNode node);
void visit(Node node);
}

如果在运行时,无论实际键入 root ,我们的调用将进入重载访问(Node node)。对于 Node 类型声明的所有变量都是如此。为什么是这样?因为Java和其他类似C语言的语言在决定要调用哪个重载时,只考虑参数的静态类型或该变量的声明类型。对于每个方法调用,Java在运行时都不需要额外的步骤,Okay,$ code> root的动态类型是什么??哦,我明白了我们来看看 MyVisitor 中是否有任何方法接受类型的参数TrainNode ...。在编译时,编译器确定哪个是被调用的方法。 (如果Java确实检查了这些参数的动态类型,性能将会非常糟糕。)



Java为我们提供了一个考虑运行时(即动态)调用方法时的对象类型 - 虚拟方法调度。当我们调用虚拟方法时,该调用实际上将转到由功能指针组成的内存中的 。每种类型都有一个表。如果一个特定的方法被一个类覆盖,那么该类'function table条目'将包含被覆盖的函数的地址。如果类不覆盖一个方法,它将包含一个指向基类实现的指针。这仍然导致性能开销(每个方法调用将基本上是取消引用两个指针:一个指向类型的函数表,另一个指向函数本身),但是它仍然比检查参数类型要快。



访问者模式的目标是完成双重派驻 - - 不仅通过虚拟方法考虑了调用目标的类型( MyVisitor ),而且还包括参数的类型(什么类型的 Node 是否正在查看)?访客模式允许我们通过访问 / 接受组合来执行此操作。



将我们的行改为:

  root.accept(new MyVisitor()); 

我们可以得到我们想要的:通过虚拟方法调度,我们输入正确的accept()调用由子类实现 - 在我们的示例中,使用 TrainElement ,我们将输入 TrainElement accept()

  class TrainNode extends Node implements IVisitable {
void接受(IVisitor v){
v.visit(this);
}
}

编译器在这一点上知道什么的范围TrainNode 接受它知道这个的静态类型是一个 TrainNode 。这是编译器在调用者的范围内不了解的一个重要的附加信息:在那里,它知道关于 root 的信息是,它是一个节点。现在编译器知道这个 root )不仅仅是一个 Node ,但实际上是一个 TrainNode 。因此,在 accept()中找到的一行: v.visit(this),完全意味着其他东西。编译器现在将查找 visit()的重载,它需要一个 TrainNode 。如果找不到一个,那么它会将调用编译为一个负载 Node 的重载。如果两者都不存在,您将收到一个编译错误(除非您有一个需要对象的重载)。执行将因此输入我们一直以来的意图: MyVisitor 执行访问(TrainNode e)。不需要演员,最重要的是,不需要反思。因此,这个机制的开销是相当低的:它只包含指针引用,没有别的。



你在你的问题是对的 - 我们可以使用一个演员并得到正确的行为。然而,通常我们甚至不知道Node是什么类型的。采取以下层次结构的情况:

 抽象类Node {...} 
抽象类BinaryNode extends Node {节点左,右; }
abstract class AdditionNode extends BinaryNode {}
abstract class MultiplicationNode extends BinaryNode {}
abstract class LiteralNode {int value;

我们正在编写一个简单的编译器来解析源文件,并生成符合以上规格。如果我们正在为访问者编写层次结构的口译员:

  class Interpreter implements IVisitor< int> {
int visit(AdditionNode n){
int left = n.left.accept(this);
int right = n.right.accept(this);
返回左+右;
}
int visit(MultiplicationNode n){
int left = n.left.accept(this);
int right = n.right.accept(this);
返回左*右;
}
int visit(LiteralNode n){
return n.value;
}
}

铸造不会让我们很远,因为我们不知道在访问()的类型 right c $ c>方法。我们的解析器很可能也只是返回一个类型为 Node 的对象,它指向了层次结构的根,所以我们也不能安全地投递。所以我们的简单的解释器可以看起来像:

  Node program = parse(args [0]); 
int result = program.accept(new Interpreter());
System.out.println(Output:+ result);

访问者模式允许我们做一些非常强大的事情:给定一个对象层次结构,它允许我们创建在层次结构上运行的模块化操作,而不需要将代码放在层次结构的类本身中。访问者模式被广泛使用,例如编译器构造。给定特定程序的语法树,许多访问者被编写为在该树上操作:类型检查,优化,机器码排放通常作为不同的访问者实现。在优化访问者的情况下,甚至可以输出给定输入树的新语法树。



它的缺点当然是:如果我们在层次结构中添加一个新类型,我们还需要添加一个 visit() code>方法,在 IVisitor 界面中,并在所有访问者中创建存根(或全部)实现。由于上述原因,我们还需要添加 accept()方法。如果表现并不意味着对您而言,则不需要 accept()就可以编写访问者的解决方案,但它们通常涉及到反射,因此可能会产生相当大的开销。


There is a lot of talk on decoupling the algorithms from the classes. But, one thing stays aside not explained.

They use visitor like this

abstract class Expr {
  public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}

class ExprVisitor extends Visitor{
  public Integer visit(Num num) {
    return num.value;
  }

  public Integer visit(Sum sum) {
    return sum.getLeft().accept(this) + sum.getRight().accept(this);
  }

  public Integer visit(Prod prod) {
    return prod.getLeft().accept(this) * prod.getRight().accept(this);
  }

Instead of calling visit(element) directly, Visitor asks the element to call its visit method. It contradicts the declared idea of class unawareness about visitors.

PS1 Please explain with your own words or point to exact explanation. Because two responses I got refer to something general and uncertain.

PS2 My guess: Since getLeft() returns the basic Expression, calling visit(getLeft()) would result in visit(Expression), whereas getLeft() calling visit(this) will result in another, more appropriate, visit invocation. So, accept() performs the type conversion (aka casting).

PS3 Scala's Pattern Matching = Visitor Pattern on Steroid shows how much simpler the Visitor pattern is without the accept method. Wikipedia adds to this statement: by linking a paper showing "that accept() methods are unnecessary when reflection is available; introduces term 'Walkabout' for the technique."

解决方案

The visitor pattern's visit/accept constructs are a necessary evil due to C-like languages' (C#, Java, etc.) semantics. The goal of the visitor pattern is to use double-dispatch to route your call as you'd expect from reading the code.

Normally when the visitor pattern is used, an object hierarchy is involved where all the nodes are derived from a base Node type, referred to henceforth as Node. Instinctively, we'd write it like this:

Node root = GetTreeRoot();
new MyVisitor().visit(root);

Herein lies the problem. If our MyVisitor class was defined like the following:

class MyVisitor implements IVisitor {
  void visit(CarNode node);
  void visit(TrainNode node);
  void visit(PlaneNode node);
  void visit(Node node);
}

If, at runtime, regardless of the actual type that root is, our call would go into the overload visit(Node node). This would be true for all variables declared of type Node. Why is this? Because Java and other C-like languages only consider the static type, or the type that the variable is declared as, of the parameter when deciding which overload to call. Java doesn't take the extra step to ask, for every method call, at runtime, "Okay, what is the dynamic type of root? Oh, I see. It's a TrainNode. Let's see if there's any method in MyVisitor which accepts a parameter of type TrainNode...". The compiler, at compile-time, determines which is the method that will be called. (If Java indeed did inspect the arguments' dynamic types, performance would be pretty terrible.)

Java does give us one tool for taking into account the runtime (i.e. dynamic) type of an object when a method is called -- virtual method dispatch. When we call a virtual method, the call actually goes to a table in memory that consists of function pointers. Each type has a table. If a particular method is overridden by a class, that class' function table entry will contain the address of the overridden function. If the class doesn't override a method, it will contain a pointer to the base class' implementation. This still incurs a performance overhead (each method call will basically be dereferencing two pointers: one pointing to the type's function table, and another of function itself), but it's still faster than having to inspect parameter types.

The goal of the visitor pattern is to accomplish double-dispatch -- not only is the type of the call target considered (MyVisitor, via virtual methods), but also the type of the parameter (what type of Node are we looking at)? The Visitor pattern allows us to do this by the visit/accept combination.

By changing our line to this:

root.accept(new MyVisitor());

We can get what we want: via virtual method dispatch, we enter the correct accept() call as implemented by the subclass -- in our example with TrainElement, we'll enter TrainElement's implementation of accept():

class TrainNode extends Node implements IVisitable {
  void accept(IVisitor v) {
    v.visit(this);
  }
}

What does the compiler know at this point, inside the scope of TrainNode's accept? It knows that the static type of this is a TrainNode. This is an important additional shred of information that the compiler was not aware of in our caller's scope: there, all it knew about root was that it was a Node. Now the compiler knows that this (root) is not just a Node, but it's actually a TrainNode. In consequence, the one line found inside accept(): v.visit(this), means something else entirely. The compiler will now look for an overload of visit() that takes a TrainNode. If it can't find one, it'll then compile the call to an overload that takes a Node. If neither exist, you'll get a compilation error (unless you have an overload that takes object). Execution will thus enter what we had intended all along: MyVisitor's implementation of visit(TrainNode e). No casts were needed, and, most importantly, no reflection was needed. Thus, the overhead of this mechanism is rather low: it only consists of pointer references and nothing else.

You're right in your question -- we can use a cast and get the correct behavior. However, often, we don't even know what type Node is. Take the case of the following hierarchy:

abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }

And we were writing a simple compiler which parses a source file and produces a object hierarchy that conforms to the specification above. If we were writing an interpreter for the hierarchy implemented as a Visitor:

class Interpreter implements IVisitor<int> {
  int visit(AdditionNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this); 
    return left + right;
  }
  int visit(MultiplicationNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this);
    return left * right;
  }
  int visit(LiteralNode n) {
    return n.value;
  }
}

Casting wouldn't get us very far, since we don't know the types of left or right in the visit() methods. Our parser would most likely also just return an object of type Node which pointed at the root of the hierarchy as well, so we can't cast that safely either. So our simple interpreter can look like:

Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);

The visitor pattern allows us to do something very powerful: given an object hierarchy, it allows us to create modular operations that operate over the hierarchy without needing requiring to put the code in the hierarchy's class itself. The visitor pattern is used widely, for example, in compiler construction. Given the syntax tree of a particular program, many visitors are written that operate on that tree: type checking, optimizations, machine code emission are all usually implemented as different visitors. In the case of the optimization visitor, it can even output a new syntax tree given the input tree.

It has its drawbacks, of course: if we add a new type into the hierarchy, we need to also add a visit() method for that new type into the IVisitor interface, and create stub (or full) implementations in all of our visitors. We also need to add the accept() method too, for the reasons described above. If performance doesn't mean that much to you, there are solutions for writing visitors without needing the accept(), but they normally involve reflection and thus can incur quite a large overhead.

这篇关于访问者模式中的accept()方法是什么意思?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆