关于将算法与类解耦,有很多讨论。但是,有一件事没有解释。
他们使用这样的访客
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);
}
访问者要求元素调用其visit方法,而不是直接调用visit(element)。它与所宣称的关于访客的阶级无意识的观念相矛盾。
PS1请用您自己的话解释或指出确切的解释。因为我得到的两个回答是指一般的和不确定的。
PS2我猜:由于getLeft()
会返回基本Expression
,因此调用visit(getLeft())
会产生visit(Expression)
,而调用getLeft()
的{{1}}将会visit(this)
导致另一个更合适的访问调用。因此,accept()
执行类型转换(也称为转换)。
PS3 Scala's Pattern Matching = Visitor Pattern on Steroid显示没有accept方法访问者模式的简单程度。 Wikipedia adds to this statement:通过链接一篇文章,显示“当反射可用时accept()
方法是不必要的;为该技术引入术语'Walkabout'。”
答案 0 :(得分:134)
访问者模式的visit
/ accept
结构是一种必要的邪恶,因为类似C的语言&#39; (C#,Java等)语义。访问者模式的目标是使用双重调度来路由您的呼叫,因为您希望阅读代码。
通常,当使用访问者模式时,涉及对象层次结构,其中所有节点都是从基本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)
。对于声明类型为Node
的所有变量,情况都是如此。为什么是这样?因为Java和其他类C语言在决定调用哪个重载时只考虑参数的静态类型或变量声明类型。对于每个方法调用,Java在运行时都没有采取额外的步骤来询问,#34;好的,root
的动态类型是什么?原来如此。它是TrainNode
。让我们看看MyVisitor
中是否有任何方法接受TrainNode
类型的参数......&#34;。编译器在编译时确定将调用哪个方法。 (如果Java确实检查了参数&#39;动态类型,性能将非常糟糕。)
Java确实为我们提供了一种工具,用于在调用方法时考虑对象的运行时(即动态)类型 - virtual method dispatch。当我们调用虚方法时,调用实际上转到内存中由函数指针组成的table。每种类型都有一张桌子。如果一个特定方法被一个类覆盖,那么该类&#39;函数表条目将包含被覆盖函数的地址。如果该类没有覆盖一个方法,它将包含一个指向基类的指针&#39;实现。这仍然会产生性能开销(每个方法调用基本上都会取消引用两个指针:一个指向类型的函数表,另一个指向函数本身),但它仍然比检查参数类型更快
访问者模式的目标是完成double-dispatch - 不仅通过虚拟方法考虑呼叫目标的类型(MyVisitor
),还考虑参数的类型(我们在看Node
的类型?访问者模式允许我们通过visit
/ accept
组合执行此操作。
将我们的行更改为:
root.accept(new MyVisitor());
我们可以得到我们想要的东西:通过虚拟方法调度,我们输入由子类实现的正确的accept()调用 - 在我们的TrainElement
示例中,我们输入{{1} 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
的实施。不需要演员阵容,最重要的是,不需要反思。因此,这种机制的开销相当低:它只包含指针引用而没有别的。
你的问题正确 - 我们可以使用演员表并获得正确的行为。但是,通常,我们甚至不知道Node是什么类型的。以下面的层次结构为例:
visit(TrainNode e)
我们正在编写一个简单的编译器,它解析源文件并生成符合上述规范的对象层次结构。如果我们为实现为访问者的层次结构编写解释器:
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()
的情况下编写访问者,但它们通常涉及反射,因此会产生相当大的开销。
答案 1 :(得分:15)
当然,如果那是唯一方式实现Accept,那将是愚蠢的。
但事实并非如此。
例如,在处理层次结构时,访问者确实非常有用在这种情况下,非终端节点的实现可能是这样的
interface IAcceptVisitor<T> {
void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
public void Accept(IVisit<T> visitor) {
visitor.visit(this);
foreach(var n in this.children)
n.Accept(visitor);
}
private IEnumerable<HierarchyNode> children;
....
}
你知道吗?您描述为愚蠢的是 遍历层次结构的解决方案。
Here is a much longer and in depth article that made me understand visitor
修改强>
澄清一下:访问者的Visit
方法包含要应用于节点的逻辑。节点的Accept
方法包含有关如何导航到相邻节点的逻辑。 仅双重调度的情况是一种特殊情况,其中没有相邻的节点可以导航到。
答案 2 :(得分:0)
访问者模式的目的是确保对象知道访问者何时完成访问并离开,因此类可以在之后执行任何必要的清理。它还允许类“暂时”将其内部暴露为“ref”参数,并且知道一旦访问者离开,内部将不再被暴露。在不需要清理的情况下,访问者模式并不十分有用。不执行这些操作的类可能无法从访问者模式中受益,但是使用访问者模式编写的代码将可用于将来可能需要在访问后进行清理的类。
例如,假设有一个数据结构包含许多应该原子更新的字符串,但是持有数据结构的类并不确切地知道应该执行什么类型的原子更新(例如,如果一个线程想要替换所有出现“X”,而另一个线程想要用数字上更高的序列替换任何数字序列,两个线程的操作都应该成功;如果每个线程只读出一个字符串,执行其更新,然后将其写回,写回字符串的第二个线程会覆盖第一个线程。实现此目的的一种方法是让每个线程获得锁,执行其操作并释放锁。不幸的是,如果以这种方式暴露锁,数据结构将无法阻止某人获取锁并且永远不会释放它。
访客模式提供(至少)三种方法来避免该问题:
如果没有访问者模式,执行原子更新将需要暴露锁并且如果调用软件无法遵循严格的锁定/解锁协议则存在失败的风险。使用访客模式,原子更新可以相对安全地完成。
答案 3 :(得分:0)
需要修改的类必须都实现'accept'方法。客户端调用此accept方法对该类系列执行一些新操作,从而扩展其功能。通过为每个特定操作传入不同的访问者类,客户端可以使用这一种接受方法来执行各种新操作。访问者类包含多个重写的访问方法,这些方法定义了如何为系列中的每个类实现相同的特定操作。这些访问方法传递给一个可以工作的实例。
如果您经常为稳定的类系列添加,更改或删除功能,则访问者非常有用,因为每个功能都是在每个访问者类别中单独定义的,而且这些类本身不需要更改。如果类的族不稳定,则访问者模式的使用可能较少,因为每次添加或删除类时,许多访问者都需要更改。
答案 4 :(得分:-1)
良好示例在源代码编译中:
interface CompilingVisitor {
build(SourceFile source);
}
客户端可以实现JavaBuilder
,RubyBuilder
,XMLValidator
等,并且无需更改用于收集和访问项目中所有源文件的实现。
如果每个源文件类型都有单独的类,那么这将是错误的模式:
interface CompilingVisitor {
build(JavaSourceFile source);
build(RubySourceFile source);
build(XMLSourceFile source);
}
归结为上下文以及您希望可扩展的系统的哪些部分。