问题描述
有很多关于将算法与类解耦的讨论.但是,一件事搁置了,没有解释.
他们使用这样的访客
abstract class Expr { public <T> T accept(Visitor<T> visitor) { return 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); }
而不是直接致电访问(元素),而是要求该元素调用其访问方法.它与宣布的阶级对访客的不认识的想法相矛盾.
ps1请用自己的话解释或指向确切的解释.因为我有两个回答,我指的是一般性和不确定的东西.
ps2我的猜测:由于getLeft()返回基本Expression,呼叫visit(getLeft())将导致visit(Expression),而getLeft() getLeft()呼叫visit(this)将导致另一个更合适的访问,请访问调用.因此,accept()执行类型转换(又称铸造).
ps3 scala scala scala scala'/a>显示访问者模式没有接受方法的简单程度. wikipedia添加到此语句中添加到此语句:通过链接一篇论文,显示" accept()方法是不必要的,当有反射时;为该技术介绍术语" Walkabout"."
推荐答案
访问者模式的visit/accept构造是必要的邪恶,这是因为C型语言(C#,Java等)语义.访问者模式的目标是使用Double-Dispatch来路由您的呼叫,如阅读代码所期望的那样.
通常,当使用访问者模式时,涉及对象层次结构,其中所有节点均来自base 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不采取额外的步骤来询问每个方法调用,在运行时,"好吧,root的动态类型是什么?哦,我明白了. MyVisitor接受类型TrainNode的参数...".编译器在编译时确定哪种方法将被调用. (如果Java确实确实检查了参数的动态类型,则性能将非常可怕.)
java确实为我们提供了一个工具,以考虑到一种方法时,可以考虑到对象的运行时(即动态)类型 - 虚拟方法调度.当我们调用虚拟方法时,呼叫实际上转到a 表.每种类型都有一个表.如果某个类的特定方法被类覆盖,则该类的功能表条目将包含覆盖函数的地址.如果类未覆盖方法,则将包含指向基类实现的指针.这仍然会造成性能开销(每个方法调用基本上都将取消两个指针:一个指向类型的功能表,另一个指向函数本身),但是它仍然比必须检查参数类型要快.
.访客模式的目标是完成 double-dispatch - 不仅是呼叫目标的类型考虑(MyVisitor,通过虚拟方法),以及参数的类型(我们正在查找哪种类型的Node)?访问者模式允许我们通过visit/accept组合做到这一点.
通过更改我们的行:
root.accept(new MyVisitor());
我们可以得到我们想要的东西:通过虚拟方法调度,我们输入了子类实现的正确Accept()调用 - 在我们的示例中,我们将输入TrainElement的实现TrainElement C6>:
class TrainNode extends Node implements IVisitable { void accept(IVisitor v) { v.visit(this); } }
编译器此时在TrainNode's accept的范围内知道什么? 它知道this的静态类型是TrainNode .这是编译器在我们的呼叫者范围中不知道的重要信息:在这里,关于root的所有信息是root是A Node.现在,编译器知道this(root)不仅是Node,而且实际上是TrainNode.结果,在accept()内部找到的一行:v.visit(this),完全意味着其他东西.现在,编译器将寻找visit()的过载> TrainNode.如果找不到一个,则将其编译为Node的过载.如果不存在,您将获得汇编错误(除非您有object的过载).因此,执行将输入我们一直打算的内容:MyVisitor的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; } }
铸造不会使我们走得更远,因为我们不知道visit()方法中的left或right的类型.我们的解析器很可能也只会返回一个类型Node的对象,该对象也指向层次结构的根部,因此我们也不能安全地施放.因此,我们的简单解释器可以看起来像:
Node program = parse(args[0]); int result = program.accept(new Interpreter()); System.out.println("Output: " + result);
访问者模式允许我们做一些非常有力的事情:给定对象层次结构,它使我们能够创建模块化操作,这些操作在层次结构上运行,而无需将代码放入层次结构的类的本身.访问者图案被广泛使用,例如在编译器构造中.鉴于特定程序的语法树,许多访问者都是在该树上操作的许多访问者:类型检查,优化,机器代码排放通常都以不同的访问者的形式实现.对于优化访问者,它甚至可以输出给定输入树的新语法树.
当然,它具有缺点:如果我们在层次结构中添加新类型,我们还需要在visit()中添加visit()方法,将新类型的方法添加到IVisitor接口中,并创建stub(或完整)实现在我们所有的访客中.出于上述原因,我们还需要添加accept()方法.如果表现对您来说并不意味着太多,那么有一些解决访问者而无需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; .... }
你看到了吗?您所说的愚蠢是 解决层次结构的解决方案.
编辑: 要澄清:访问者的Visit方法包含要应用于节点的逻辑.节点的Accept方法包含有关如何导航到相邻节点的逻辑.您唯一的 double Dispatch是一种特殊情况,没有相邻节点可以导航到.
其他推荐答案
访问者模式的目的是确保对象知道访问者何时与他们一起完成并已离开,因此这些类可以在此后进行任何必要的清理.它还允许课程"暂时"作为" REF"参数公开其内部设备,并知道一旦访客消失,内部设备将不再暴露.如果不需要清理,则访问者模式并不是非常有用.这些事情都没有从访问者模式中受益的类,但是编写用于使用访问者模式的代码将适用于未来的类,这些类可能需要清理后.
例如,假设一个人具有一个数据结构,持有许多字符串,应在原子上进行更新,但是持有数据结构的类并不确切地知道应该执行哪些类型的原子更新(例如,如果一个线程要替换所有内容出现" x",而另一个线程则希望用数值更高的序列替换任何数字序列,两个线程的操作都应成功;如果每个线程简单地读出一个字符串,执行其更新并将其写回,则写回其字符串的第二个线程将覆盖第一个).实现此目的的一种方法是让每个线程获取锁,执行其操作并释放锁.不幸的是,如果以这种方式暴露锁,数据结构将无法阻止某人获取锁并永远不会释放锁.
访客模式(至少)提供了三种方法来避免该问题:
- 它可以锁定记录,调用所提供的功能,然后解锁记录;如果所提供的功能落入无尽的循环,则可以将记录永远锁定,但是如果所提供的功能返回或抛出异常,则记录将被解锁(如果该功能引发异常,则可以将记录标记为无效;它锁定可能不是一个好主意).请注意,重要的是,如果调用功能试图获取其他锁,则可能会导致僵局.
- 在某些平台上,它可以通过存储位置将字符串作为" REF"参数传递.然后,该功能可以复制字符串,根据复制的字符串计算新字符串,尝试将旧字符串比较旧字符串与新字符串进行比较,并在CompareEexchange失败时重复整个过程.
- 它可以制作字符串的副本,调用字符串上提供的功能,然后使用compareexchange本身尝试更新原始功能,并在complateeexchange失败时重复整个过程.
没有访问者模式,如果通话软件未能遵循严格的锁定/解锁协议,则执行原子更新将需要暴露锁和失败的风险.使用访问者模式,可以相对安全地进行原子更新.
问题描述
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) { return 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.
其他推荐答案
Of course that would be silly if that was the only way that Accept is implemented.
But it is not.
For example, visitors are really really useful when dealing with hierarchies in which case the implementation of a non-terminal node might be something like this
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; .... }
You see? What you describe as stupid is the solution for traversing hierarchies.
Here is a much longer and in depth article that made me understand visitor.
Edit: To clarify: The visitor's Visit method contains logic to be applied to a node. The node's Accept method contains logic on how to navigate to adjacent nodes. The case where you only double dispatch is a special case where there are simply no adjacent nodes to navigate to.
其他推荐答案
The purpose of the Visitor pattern is to ensure that objects know when the visitor is finished with them and have departed, so the classes can perform any necessary cleanup afterward. It also allows classes to expose their internals "temporarily" as 'ref' parameters, and know that the internals will no longer be exposed once the visitor is gone. In cases where no cleanup is necessary, the visitor pattern isn't terribly useful. Classes which do neither of these things may not benefit from the visitor pattern, but code which is written to use the visitor pattern will be usable with future classes that may require cleanup after access.
For example, suppose one has a data structure holding many strings that should be updated atomically, but the class holding the data structure doesn't know precisely what types of atomic updates should be performed (e.g. if one thread wants to replace all occurrences of "X", while another thread wants to replace any sequence of digits with a sequence that is numerically one higher, both threads' operations should succeed; if each thread simply read out a string, performed its updates, and wrote it back, the second thread to write back its string would overwrite the first). One way to accomplish this would be to have each thread acquire a lock, perform its operation, and release the lock. Unfortunately, if locks are exposed in that way, the data structure would have no way of preventing someone from acquiring a lock and never releasing it.
The Visitor pattern offers (at least) three approaches to avoid that problem:
- It can lock a record, call the supplied function, and then unlock the record; the record could be locked forever if the supplied function falls into an endless loop, but if the supplied function returns or throws an exception, the record will be unlocked (it may be reasonable to mark the record invalid if the function throws an exception; leaving it locked is probably not a good idea). Note that it's important that if the called function attempts to acquire other locks, deadlock could result.
- On some platforms, it can pass a storage location holding the string as a 'ref' parameter. That function could then copy the string, compute a new string based upon the copied string, attempt to CompareExchange the old string to the new one, and repeat the whole process if the CompareExchange fails.
- It can make a copy of the string, call the supplied function on the string, then use CompareExchange itself to attempt to update the original, and repeat the whole process if the CompareExchange fails.
Without the visitor pattern, performing atomic updates would require exposing locks and risking failure if calling software fails to follow a strict locking/unlocking protocol. With the Visitor pattern, atomic updates can be done relatively safely.