รูปแบบผู้เยี่ยมชมvisit
/accept
โครงสร้างเป็นสิ่งชั่วร้ายที่จำเป็นเนื่องจากความหมายของภาษาซี (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 ให้เครื่องมือหนึ่งแก่เราในการพิจารณาประเภทรันไทม์ (เช่นไดนามิก) ของอ็อบเจ็กต์เมื่อมีการเรียกเมธอด - virtual methodวิธีการจัดส่งเสมือนเมื่อเราเรียกวิธีการเสมือนจริงการเรียกจะไปที่ตารางในหน่วยความจำที่ประกอบด้วยตัวชี้ฟังก์ชัน แต่ละประเภทมีตาราง ถ้าเมธอดเฉพาะถูกแทนที่โดยคลาสรายการตารางฟังก์ชันของคลาสนั้นจะมีแอดเดรสของฟังก์ชันที่ถูกแทนที่ หากคลาสไม่ได้แทนที่เมธอดคลาสนั้นจะมีตัวชี้ไปที่การใช้งานคลาสพื้นฐาน สิ่งนี้ยังคงมีค่าใช้จ่ายด้านประสิทธิภาพ (โดยทั่วไปการเรียกแต่ละวิธีจะเป็นการยกเลิกการอ้างอิงสองพอยน์เตอร์: อันหนึ่งชี้ไปที่ตารางฟังก์ชันของประเภทและอีกฟังก์ชันหนึ่งเอง) แต่ก็ยังเร็วกว่าการตรวจสอบประเภทพารามิเตอร์
เป้าหมายของรูปแบบผู้เยี่ยมชมคือการบรรลุการจัดส่งแบบสองครั้ง - ไม่เพียง แต่พิจารณาประเภทของเป้าหมายการโทร ( MyVisitor
ผ่านวิธีการเสมือน) แต่ยังรวมถึงประเภทของพารามิเตอร์ด้วย ( Node
เรากำลังดูประเภทใด) รูปแบบผู้เยี่ยมชมช่วยให้เราสามารถทำได้โดยใช้visit
/ accept
combination
โดยเปลี่ยนบรรทัดของเราเป็นสิ่งนี้:
root.accept(new MyVisitor());
เราจะได้รับสิ่งที่ต้องการ: ผ่านวิธีการจัดส่งเสมือนเราป้อนการเรียกยอมรับ () ที่ถูกต้องตามที่คลาสย่อยดำเนินการ - ในตัวอย่างของTrainElement
เราเราจะเข้าสู่TrainElement
การใช้งานaccept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
อะไรคอมไพเลอร์รู้ที่จุดนี้อยู่ภายในขอบเขตของTrainNode
's accept
? มันรู้ว่าประเภทคงที่ของthis
คือTrainNode
. นี่เป็นข้อมูลเพิ่มเติมที่สำคัญที่คอมไพเลอร์ไม่ทราบในขอบเขตของผู้โทรของเรา: ที่นั่นทั้งหมดที่รู้root
ก็คือมันเป็นไฟล์Node
. ตอนนี้คอมไพเลอร์รู้แล้วว่าthis
( root
) ไม่ใช่แค่ a Node
แต่จริงๆแล้วมันคือไฟล์TrainNode
. ดังนั้นบรรทัดเดียวที่พบภายในaccept()
: v.visit(this)
หมายถึงอย่างอื่นทั้งหมด ตอนนี้คอมไพเลอร์จะมองหาการโอเวอร์โหลดvisit()
ที่ใช้เวลา a TrainNode
. หากไม่พบมันจะรวบรวมการโทรไปยังโอเวอร์โหลดที่ใช้เวลา aNode
:หากไม่มีอยู่คุณจะได้รับข้อผิดพลาดในการคอมไพล์ (เว้นแต่คุณจะมีโอเวอร์โหลด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()
แต่โดยปกติจะเกี่ยวข้องกับการไตร่ตรองจึงอาจมีค่าใช้จ่ายค่อนข้างมาก