Ziyaretçi kalıbı visit
/ accept
yapıları, C benzeri dillerin (C #, Java, vb.) Semantiği nedeniyle gerekli bir kötülüktür. Ziyaretçi modelinin amacı, aramanızı kodu okumaktan bekleyeceğiniz şekilde yönlendirmek için çift gönderimi kullanmaktır.
Normalde ziyaretçi modeli kullanıldığında, tüm düğümlerin Node
bundan sonra olarak anılan bir temel türden türetildiği bir nesne hiyerarşisi dahil edilir Node
. İçgüdüsel olarak şöyle yazardık:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
Sorun burada yatıyor. Bizim ise MyVisitor
sınıf aşağıdaki gibi tanımlanmıştır:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
Çalışma zamanında, gerçek türden bağımsız olarak, root
çağrımız aşırı yüke girecektir visit(Node node)
. Bu, türden bildirilen tüm değişkenler için geçerli olacaktır Node
. Bu neden? Çünkü Java ve diğer C benzeri diller , hangi aşırı yüklemenin çağrılacağına karar verirken parametrenin yalnızca statik türünü veya değişkenin bildirildiği türü dikkate alır. Java, her yöntem çağrısı için çalışma zamanında "Tamam, dinamik türü root
nedir? Ah, anlıyorum. Bu bir TrainNode
. Bakalım MyVisitor
bir tür parametresini kabul eden herhangi bir yöntem var mı?TrainNode
... ". Derleyici, derleme zamanında, hangi yöntemin çağrılacağını belirler. (Java, argümanların dinamik türlerini gerçekten incelemiş olsaydı, performans oldukça kötü olurdu.)
Java, bir yöntem çağrıldığında bir nesnenin çalışma zamanı (yani dinamik) türünü hesaba katmak için bize bir araç sağlar - sanal yöntem gönderimi . Sanal bir yöntemi çağırdığımızda, çağrı aslında bellekte işlev işaretçilerinden oluşan bir tabloya gider . Her türün bir tablosu vardır. Belirli bir yöntem bir sınıf tarafından geçersiz kılınırsa, bu sınıfın işlev tablosu girişi, geçersiz kılınan işlevin adresini içerecektir. Sınıf bir yöntemi geçersiz kılmazsa, temel sınıfın uygulanmasına bir işaretçi içerecektir. Bu yine de bir performans ek yüküne neden olur (her yöntem çağrısı temelde iki işaretleyicinin referansını kaldırır: biri türün işlev tablosunu, diğeri işlevin kendisini gösterir), ancak yine de parametre türlerini incelemekten daha hızlıdır.
Ziyaretçi modelinin amacı, çift gönderimi gerçekleştirmektir - yalnızca dikkate alınan çağrı hedefinin türü değil ( MyVisitor
sanal yöntemler aracılığıyla), aynı zamanda parametrenin türü (ne tür Node
arıyoruz)? Ziyaretçi kalıbı, bunu visit
/ accept
kombinasyonu ile yapmamızı sağlar .
Hattımızı buna değiştirerek:
root.accept(new MyVisitor());
İstediğimizi elde edebiliriz: sanal yöntem gönderimi yoluyla, alt sınıf tarafından uygulandığı şekliyle doğru accept () çağrısını giriyoruz - örneğimizde aşağıdakilerin uygulamasını TrainElement
gireceğiz :TrainElement
accept()
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
Derleyici bu noktada, TrainNode
'ın kapsamı içinde ne biliyor accept
? Statik türünün this
a olduğunu bilirTrainNode
. Bu, derleyicinin arayanın kapsamı içinde farkında olmadığı önemli bir ek bilgi parçacığıdır: Orada, tek bildiği root
bir Node
. Artık derleyici this
( root
) 'nin sadece a olmadığını Node
, aslında bir olduğunu biliyor TrainNode
. Sonuç olarak, içinde bulunan tek satır accept()
: v.visit(this)
tamamen başka bir şey anlamına gelir. Derleyici şimdi visit()
bir TrainNode
. Bir tane bulamazsa, çağrıyı birNode
. Hiçbiri yoksa, bir derleme hatası alırsınız (süren bir aşırı yüklemeniz yoksa object
). Yürütme, böylece başından beri niyet ettiğimiz şeye girecektir: MyVisitor
'ın uygulanmasına visit(TrainNode e)
. Hiçbir alçıya gerek yoktu ve en önemlisi, herhangi bir yansımaya gerek yoktu. Bu nedenle, bu mekanizmanın ek yükü oldukça düşüktür: yalnızca işaretçi referanslarından oluşur ve başka hiçbir şey yoktur.
Sorunuzda haklısınız - bir alçı kullanabilir ve doğru davranışı alabiliriz. Ancak, çoğu zaman, Düğümün ne tür olduğunu bile bilmiyoruz. Aşağıdaki hiyerarşinin durumunu ele alalım:
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; }
Ve bir kaynak dosyayı ayrıştıran ve yukarıdaki spesifikasyona uyan bir nesne hiyerarşisi üreten basit bir derleyici yazıyorduk. Ziyaretçi olarak uygulanan hiyerarşi için bir tercüman yazıyor olsaydık:
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;
}
}
Biz türlerini bilmiyoruz çünkü çok uzak bize almak değildir Döküm left
veya right
içinde visit()
yöntemlerle. Ayrıştırıcımız büyük olasılıkla Node
hiyerarşinin köküne işaret eden bir tür nesnesi de döndürecektir , bu yüzden onu da güvenli bir şekilde çeviremeyiz. Böylece basit tercümanımız şöyle görünebilir:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
Ziyaretçi kalıbı, çok güçlü bir şey yapmamızı sağlar: bir nesne hiyerarşisi verildiğinde, kodu hiyerarşinin sınıfına koymaya gerek kalmadan hiyerarşi üzerinde çalışan modüler işlemler oluşturmamıza izin verir. Ziyaretçi kalıbı, örneğin derleyici yapımında yaygın olarak kullanılır. Belirli bir programın sözdizimi ağacı göz önüne alındığında, o ağaç üzerinde çalışan pek çok ziyaretçi yazılır: tür kontrolü, optimizasyonlar, makine kodu yayımı genellikle farklı ziyaretçiler olarak uygulanır. Optimizasyon ziyaretçisi durumunda, giriş ağacına göre yeni bir sözdizimi ağacı bile çıkarabilir.
Elbette dezavantajları var: hiyerarşiye yeni bir tür eklersek visit()
, IVisitor
arayüze bu yeni tür için bir yöntem de eklememiz ve tüm ziyaretçilerimizde saplama (veya tam) uygulamalar oluşturmamız gerekir. accept()
Yukarıda açıklanan nedenlerden dolayı yöntemi de eklememiz gerekiyor . Performans sizin için bu kadar önemli değilse, ziyaretçilere ihtiyaç duymadan yazmak için çözümler vardır accept()
, ancak bunlar normalde yansıma içerir ve bu nedenle oldukça büyük bir ek yüke neden olabilirler.