ビジターパターンのvisit
/accept
構成は、Cのような言語(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は、実行時にすべてのメソッド呼び出しに対して、「オーケー、動的タイプはroot
何ですか?ああ、わかりました。それはTrainNode
です。MyVisitor
タイプのパラメーターを受け入れるメソッドがあるかどうかを確認するための追加の手順を実行しません。TrainNode
... "。コンパイラは、コンパイル時に、呼び出されるメソッドを決定します(Javaが実際に引数の動的型を検査した場合、パフォーマンスはかなりひどいものになります。)
Javaは、メソッドが呼び出されたときにオブジェクトの実行時(つまり動的)タイプを考慮するための1つのツールを提供します-仮想メソッドディスパッチ。仮想メソッドを呼び出すと、その呼び出しは実際には関数ポインターで構成されるメモリ内のテーブルに送られます。各タイプにはテーブルがあります。特定のメソッドがクラスによってオーバーライドされた場合、そのクラスの関数テーブルエントリには、オーバーライドされた関数のアドレスが含まれます。クラスがメソッドをオーバーライドしない場合、基本クラスの実装へのポインターが含まれます。これでもパフォーマンスのオーバーヘッドが発生します(各メソッド呼び出しは基本的に2つのポインターを逆参照します。1つは型の関数テーブルを指し、もう1つは関数自体を指します)が、パラメーター型を検査するよりも高速です。
ビジターパターンの目標は、ダブルディスパッチを達成することです-呼び出しターゲットのタイプが考慮されるだけでなく(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()
:の中にある1行は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;
}
}
我々はの種類がわからないので、キャストすると、非常に遠く、私たちを取得していないでしょうleft
かright
におけるvisit()
方法を。私たちのパーサーはNode
、階層のルートを指すタイプのオブジェクトも返す可能性が高いため、それを安全にキャストすることもできません。したがって、単純なインタプリタは次のようになります。
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
ビジターパターンを使用すると、非常に強力な処理を実行できます。オブジェクト階層が与えられると、階層のクラス自体にコードを配置しなくても、階層上で動作するモジュラー操作を作成できます。ビジターパターンは、コンパイラーの構築などで広く使用されています。特定のプログラムの構文ツリーを考えると、そのツリーを操作する多くの訪問者が作成されます。型チェック、最適化、マシンコードの発行はすべて、通常、異なる訪問者として実装されます。最適化ビジターの場合、入力ツリーを指定して新しい構文ツリーを出力することもできます。
もちろん、欠点もあります。階層に新しい型を追加する場合はvisit()
、その新しい型のメソッドもIVisitor
インターフェイスに追加し、すべての訪問者にスタブ(または完全)実装を作成する必要があります。accept()
上記の理由から、メソッドも追加する必要があります。パフォーマンスがそれほど重要でない場合はaccept()
、を必要とせずに訪問者を書き込むためのソリューションがありますが、通常はリフレクションを伴うため、かなり大きなオーバーヘッドが発生する可能性があります。