Das Besuchermuster visit
/accept
Konstrukte sind ein notwendiges Übel aufgrund von C-ähnliche Sprachen (C #, Java, etc.) Semantik. Das Ziel des Besuchermusters besteht darin, Ihren Anruf mithilfe des doppelten Versands so weiterzuleiten, wie Sie es vom Lesen des Codes erwarten.
Normalerweise ist bei Verwendung des Besuchermusters eine Objekthierarchie beteiligt, bei der alle Knoten von einem Basistyp abgeleitet sind Node
, der im Folgenden als bezeichnet wird Node
. Instinktiv würden wir es so schreiben:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
Hierin liegt das Problem. Wenn unsere MyVisitor
Klasse wie folgt definiert wurde:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
Wenn zur Laufzeit, unabhängig vom tatsächlichen Typ root
, unser Aufruf in die Überlastung gehen würde visit(Node node)
. Dies gilt für alle vom Typ deklarierten Variablen Node
. Warum ist das? Da Java und andere C-ähnliche Sprachen bei der Entscheidung, welche Überladung aufgerufen werden soll, nur den statischen Typ oder den Typ berücksichtigen, als den die Variable deklariert ist. Java unternimmt nicht den zusätzlichen Schritt, um bei jedem Methodenaufruf zur Laufzeit zu fragen: "Okay, was ist der dynamische Typ root
? Oh, ich verstehe. Es ist einTrainNode
. Mal sehen, ob es eine Methode inMyVisitor
die einen Parameter vom Typ akzeptiertTrainNode
... ". Der Compiler bestimmt zur Kompilierungszeit, welche Methode aufgerufen wird. (Wenn Java tatsächlich die dynamischen Typen der Argumente untersuchen würde, wäre die Leistung ziemlich schrecklich.)
Java bietet uns ein Tool, mit dem der Laufzeittyp (dh der dynamische Typ) eines Objekts beim Aufruf einer Methode berücksichtigt werden kann - der Versand virtueller Methoden . Wenn wir eine virtuelle Methode aufrufen, wird der Aufruf tatsächlich an eine Tabelle im Speicher gesendet, die aus Funktionszeigern besteht. Jeder Typ hat eine Tabelle. Wenn eine bestimmte Methode von einer Klasse überschrieben wird, enthält der Funktionstabelleneintrag dieser Klasse die Adresse der überschriebenen Funktion. Wenn die Klasse eine Methode nicht überschreibt, enthält sie einen Zeiger auf die Implementierung der Basisklasse. Dies verursacht immer noch einen Leistungsaufwand (bei jedem Methodenaufruf werden im Wesentlichen zwei Zeiger dereferenziert: einer zeigt auf die Funktionstabelle des Typs und einer auf die Funktion selbst), aber es ist immer noch schneller, als Parametertypen untersuchen zu müssen.
Das Ziel des Besuchermusters ist es, einen doppelten Versand zu erreichen - wird nicht nur der Typ des Anrufziels berücksichtigt ( MyVisitor
über virtuelle Methoden), sondern auch der Typ des Parameters (welche Art von betrachten Node
wir)? Das Besuchermuster ermöglicht es uns, dies durch die Kombination visit
/ zu tun accept
.
Indem Sie unsere Linie dahingehend ändern:
root.accept(new MyVisitor());
Wir können bekommen, was wir wollen: Über den Versand virtueller Methoden geben wir den richtigen accept () -Aufruf ein, wie er von der Unterklasse implementiert wurde. In unserem Beispiel mit geben TrainElement
wir die TrainElement
Implementierung von ein accept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
Was weiß der Compiler an dieser Stelle im Rahmen von TrainNode
's accept
? Es weiß, dass der statische Typ von a this
istTrainNode
. Dies ist eine wichtige zusätzliche Information, die dem Compiler im Bereich unseres Aufrufers nicht bekannt war: Dort wusste er root
nur, dass es sich um eine handelt Node
. Jetzt weiß der Compiler, dass this
( root
) nicht nur ein Node
, sondern tatsächlich ein ist TrainNode
. Infolgedessen bedeutet die eine Zeile im Inneren accept()
: v.visit(this)
etwas ganz anderes. Der Compiler sucht nun nach einer Überladung visit()
, die a benötigt TrainNode
. Wenn es keinen findet, kompiliert es den Aufruf zu einer Überladung, die a benötigtNode
. Wenn beides nicht vorhanden ist, wird ein Kompilierungsfehler angezeigt (es sei denn, Sie haben eine Überladung, die erforderlich ist object
). Die Ausführung wird somit in das eingehen, was wir die ganze Zeit beabsichtigt hatten: MyVisitor
die Implementierung von visit(TrainNode e)
. Es waren keine Abgüsse erforderlich, und vor allem war keine Reflexion erforderlich. Daher ist der Aufwand für diesen Mechanismus eher gering: Er besteht nur aus Zeigerreferenzen und sonst nichts.
Sie haben Recht mit Ihrer Frage - wir können eine Besetzung verwenden und das richtige Verhalten erzielen. Oft wissen wir jedoch nicht einmal, um welchen Knotentyp es sich handelt. Nehmen Sie den Fall der folgenden Hierarchie:
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; }
Und wir haben einen einfachen Compiler geschrieben, der eine Quelldatei analysiert und eine Objekthierarchie erzeugt, die der obigen Spezifikation entspricht. Wenn wir einen Interpreter für die als Besucher implementierte Hierarchie schreiben würden:
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 würde uns nicht sehr weit bringen, da wir die Arten left
oder right
in den visit()
Methoden nicht kennen . Unser Parser würde höchstwahrscheinlich auch nur ein Objekt vom Typ zurückgeben, Node
das ebenfalls auf die Wurzel der Hierarchie zeigt, sodass wir dies auch nicht sicher umsetzen können. Unser einfacher Dolmetscher kann also so aussehen:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
Das Besuchermuster ermöglicht es uns, etwas sehr Mächtiges zu tun: Bei einer gegebenen Objekthierarchie können wir modulare Operationen erstellen, die über die Hierarchie arbeiten, ohne dass der Code in die Klasse der Hierarchie selbst eingefügt werden muss. Das Besuchermuster wird beispielsweise in der Compilerkonstruktion häufig verwendet. In Anbetracht des Syntaxbaums eines bestimmten Programms werden viele Besucher geschrieben, die diesen Baum bearbeiten: Typprüfung, Optimierungen und Emission von Maschinencode werden normalerweise als unterschiedliche Besucher implementiert. Im Falle des Optimierungsbesuchers kann er anhand des Eingabebaums sogar einen neuen Syntaxbaum ausgeben.
Das hat natürlich seine Nachteile: Wenn wir der Hierarchie einen neuen Typ hinzufügen, müssen wir visit()
der IVisitor
Schnittstelle auch eine Methode für diesen neuen Typ hinzufügen und bei allen unseren Besuchern Stub- (oder vollständige) Implementierungen erstellen. accept()
Aus den oben beschriebenen Gründen müssen wir auch die Methode hinzufügen . Wenn Ihnen die Leistung nicht so viel bedeutet, gibt es Lösungen für das Schreiben von Besuchern, ohne die zu benötigen. accept()
Diese beinhalten jedoch normalerweise Reflexionen und können daher einen ziemlich hohen Overhead verursachen.