由于C语言(C#,Java等)的语义,访问者模式的visit/accept构造是必不可少的。访客模式的目标是使用双分派来路由您的呼叫,如您阅读代码所期望的那样。
通常,当使用访问者模式时,将涉及对象层次结构,其中所有节点均从基本Node类型(此后称为)派生Node。本能地,我们会这样写:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
问题就在这里。如果我们的MyVisitor类定义如下:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
如果在运行时不管实际类型root是什么,我们的调用将进入重载状态visit(Node node)。对于声明为type的所有变量都是如此Node。为什么是这样?因为Java和其他类似C的语言在确定要调用的重载时仅考虑参数的静态类型或将变量声明为的类型。Java并不需要采取额外的步骤来询问每个方法调用,在运行时,“好吧,动态类型是root什么?哦,我知道了。它是一个TrainNode。让我们看看是否有任何方法MyVisitor接受类型参数TrainNode...”。编译器在编译时确定将调用哪个方法。(如果Java确实检查了参数的动态类型,性能将非常糟糕。)
Java确实为我们提供了一种工具,用于在方法调用时考虑对象的运行时(即动态)类型-虚拟方法调度。当我们调用虚拟方法时,调用实际上转到内存中由函数指针组成的表。每种类型都有一个表格。如果某个类覆盖了某个特定方法,则该类的函数表条目将包含该覆盖函数的地址。如果该类未覆盖方法,它将包含一个指向基类实现的指针。这仍然会带来性能开销(每个方法调用将基本上取消引用两个指针:一个指向类型的函数表,另一个指向函数本身),但是它比必须检查参数类型要快。
访客模式的目标是完成两次调度-不仅要考虑调用目标的类型(MyVisitor通过虚拟方法),而且还要考虑参数的类型(Node我们要查看的是哪种类型)?访客模式允许我们通过visit/accept组合执行此操作。
通过将我们的行更改为此:
root.accept(new MyVisitor());
我们可以得到想要的:通过虚拟方法分派,我们输入由子类实现的正确的accept()调用-在我们的示例中TrainElement,我们将输入TrainElement的实现accept():
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
什么编译器知道在这一点上,范围内TrainNode的accept? 它知道的静态类型this是TrainNode。这是编译器在调用者范围内不了解的另一重要重要信息:在那里,它所知道的root只是一个Node。现在,编译器知道this(root)不仅是一个Node,而且实际上是一个TrainNode。因此,在accept():中找到的一行v.visit(this)完全意味着其他内容。编译器现在将寻找过载的visit(),需要一个TrainNode。如果找不到,则会将调用编译为需要一个Node。如果两者都不存在,则将出现编译错误(除非您有接受的重载object)。因此执行将进入我们一直以来的预期目标:MyVisitor的实现visit(TrainNode e)。不需要强制转换,最重要的是,不需要反射。因此,此机制的开销相当低:它仅包含指针引用,而没有其他内容。
您的问题是对的-我们可以使用强制转换并获得正确的行为。但是,通常,我们甚至不知道Node是什么类型。以以下层次结构为例:
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; }
我们正在编写一个简单的编译器,该编译器分析源文件并生成符合上述规范的对象层次结构。如果我们正在为实现为访问者的层次结构编写解释器:
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;
}
}
铸造不会让我们很远,因为我们不知道类型的left或者right在visit()方法。我们的解析器很可能还会返回类型Node也指向层次结构根的对象,因此我们也无法安全地进行转换。因此,我们的简单解释器可能如下所示:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
访问者模式使我们能够执行非常强大的操作:在给定对象层次结构的情况下,它允许我们创建对层次结构进行操作的模块化操作,而无需将代码放入层次结构的类本身中。访客模式被广泛使用,例如,在编译器构造中。在给定特定程序的语法树的情况下,将编写许多在该树上运行的访问者:类型检查,优化,机器代码发布通常都是作为不同的访问者实现的。对于优化访问者,给定输入树,它甚至可以输出新的语法树。
当然,它有缺点:如果我们在层次结构中添加新类型,我们还需要visit()在IVisitor接口中添加针对该新类型的方法,并在所有访问者中创建存根(或完整)实现。accept()由于上述原因,我们也需要添加该方法。如果性能对您没有太大的意义,则可以使用一些解决方案来编写访问者而不需要accept(),但是它们通常涉及反射,因此会产生相当大的开销。