방문자 패턴의 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
호출이 overload로 들어가면 visit(Node node)
. 이것은 type으로 선언 된 모든 변수에 적용됩니다 Node
. 왜 이런거야? Java 및 기타 C 계열 언어 는 호출 할 오버로드를 결정할 때 매개 변수 의 정적 유형 또는 변수가 선언 된 유형 만 고려 하기 때문입니다. Java는 런타임시 모든 메서드 호출에 대해 "좋아요, 동적 유형이 root
무엇입니까? "라는 추가 단계를 거치지 않습니다 . 유형의 매개 변수를 허용하는 TrainNode
메서드가 있는지 살펴 보겠습니다.MyVisitor
TrainNode
... ". 컴파일러는 컴파일 타임에 호출 될 메서드를 결정합니다. (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()
있지만 일반적으로 반사가 수반되므로 상당한 오버 헤드가 발생할 수 있습니다.