Ders 04 / 15
Nesne Güvenliği ve Dinamik Gönderim
Bir trait sınırıyla yazılmış çağrı derleme zamanında, bir trait nesnesi (dyn) üzerinden yazılmış çağrı koşum sırasında çözülür; ölçüm çekirdeğinde ikisi de iki kez koşar — fark sayıda değil, çağrının nasıl bulunduğundadır ve bu, yalnız nesne güvenliği koşulunu sağlayan traitler için mümkündür.
Yayın Güncelleme
Bu derste
- Statik ve dinamik gönderimin sözdizimini (trait sınırı ile dyn trait nesnesi) ayırt eder
- İki gönderim biçiminin koşum sayısını karşılaştırarak sayının değişmediğini gösterir
- Nesne güvenliği koşulunu düzyazıyla tarif eder ve bu koşulu sağlayan kabul edilmiş bir tasarımı koşturarak gösterir
İçindekiler
İlişkili Tipler dersi, bir sözleşmeyi kullanmanın iki yolunun da (ilişkili tip, genellik
parametresi) derleme zamanında çözüldüğünü gösterdi. Bir trait’i kullanmanın üçüncü bir
yolu var ve bu yol derleme zamanında değil koşum sırasında çözülüyor: trait nesnesi.
Kursun ilk dersinde koşan çekirdekte bu ayrım zaten vardı — statik<T: Etiket> ile
dinamik(x: &dyn Etiket) aynı işi iki farklı çağrı biçimiyle yapıyordu. Bu ders o farkı
açıyor: çağrı ne zaman çözülürse çözülsün, gövde kaç kez koşuyor?
İki Çağrı Biçimi
statik<T: Etiket>(x: &T) yazımı önceki iki dersten tanıdık: T bir tip parametresi, sınırı
Etiket. Derleyici bu fonksiyonu her çağrıldığı somut tip için ayrı ayrı üretiyor — Trait
Sınırları dersinin gösterdiği gibi. dinamik(x: &dyn Etiket) yazımı farklı: xin tipi artık
bir tip parametresi değil, dyn Etiket — “Etiketi gerçekleştiren, tipi koşum sırasında
belirlenecek herhangi bir değere referans.” dyn anahtar sözcüğü, derleyiciye “bu referansın
arkasındaki somut tipi şimdiden bilmiyorum, ama Etiketin yöntemlerini çağırabileceğimi
biliyorum” demenin yolu.
Bu iki biçimin var olma nedeni farklı sorulara yanıt vermeleri. Statik gönderim, çağrılan her
somut tip için ayrı bir gövde üretiyor — Trait Sınırları dersinin gösterdiği gibi, bu üretim
derleme zamanında olur ve koşum defterine hiç girmez. Ama bu üretim bir bedel de taşır: bir
fonksiyon <T: Etiket> yazdığında, o fonksiyonu çağıran her yer, hangi somut Tnin
kullanıldığını derleme zamanında bilmek zorundadır. Bir liste, bir alan ya da bir dönüş değeri
“ya Sade ya Ozel olabilir” demek istiyorsa, statik gönderim bunu ifade edemez — tip
parametresi tek bir somut tipe çözülmek zorundadır. Dinamik gönderim tam bu boşluğu
dolduruyor: dyn Etiket, “hangi somut tip olduğu önemli değil, yeter ki Etiketi
gerçekleştirsin” diyerek farklı tipleri aynı yerde tutmayı mümkün kılıyor. Bu ders ikisinin
birbirinin yerine geçtiğini değil, birbirini tamamladığını gösteriyor.
Bu iki yazımın karşılığı ikili düzeyde de farklı. statik çağrıldığında derleyici hangi
yöntemin çalışacağını kaynaktan doğrudan okuyup çağrıya gömüyor. dinamik çağrıldığında ise
xin beraberinde bir yöntem tablosu taşıması gerekiyor — trait nesnesi aslında iki
parçadan oluşuyor: değerin kendisine bir referans ve o değerin somut tipine ait yöntemlerin
adreslerini tutan bir tablo. Çağrı sırasında hangi gövdenin çalışacağı bu tablodan
bulunuyor; kaynakta yazan tek satır (x.ad()), xin arkasındaki tipe göre iki farklı
gövdeye açılabiliyor.
Box<dyn Etiket> iki kelimeden kuruluyor ve ikisi de ayrı bir kursta zaten tanıtıldı. Box,
öbekte tekil sahiplik kuran bir kap: içindeki değeri öbeğe yerleştirir ve tek bir sahip
tutar. dyn Etiket ise bu kabın içinde hangi somut tipin durduğunu adlandırmıyor, yalnız
hangi sözleşmeyi karşıladığını söylüyor. İkisi birleştiğinde ortaya çıkan Box<dyn Etiket>,
“öbekte duran, Etiketi gerçekleştiren, ama tam olarak hangi tip olduğu unutulmuş bir değer”
anlamına geliyor. Bu unutma bilinçli: Box<Sade> ile Box<Ozel> derleyici için iki farklı
tiptir ve aynı diziye konamaz; Box<dyn Etiket> ikisinin de ortak paydası olduğu için aynı
diziye konabiliyor.
Koşum Sayısı Değişmiyor
// kere.rs — kesit: statik ve dinamik gonderim icin Etiket, Sade, Ozel.
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
pub type Defter = Rc<RefCell<BTreeMap<String, u32>>>;
pub fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
pub fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
pub trait Etiket {
fn defteri(&self) -> &Defter;
fn ad(&self) -> String {
String::from("varsayilan")
}
}
pub struct Sade {
pub defter: Defter,
}
impl Etiket for Sade {
fn defteri(&self) -> &Defter {
&self.defter
}
}
pub struct Ozel {
pub defter: Defter,
}
impl Etiket for Ozel {
fn defteri(&self) -> &Defter {
&self.defter
}
fn ad(&self) -> String {
String::from("ozel")
}
}
pub fn statik<T: Etiket>(x: &T) -> String {
say(x.defteri(), "statik-gonderim");
x.ad()
}
pub fn dinamik(x: &dyn Etiket) -> String {
say(x.defteri(), "dinamik-gonderim");
x.ad()
}
// gonderim.rs — statik ve dinamik gonderimin kosum sayisini karsilastirir.
mod kere;
use kere::{kere, say, Defter, Etiket, Ozel, Sade, dinamik, statik};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
let sade = Sade { defter: Rc::clone(&defter) };
let ozel = Ozel { defter: Rc::clone(&defter) };
let _ = statik(&sade);
let _ = statik(&sade);
let _ = dinamik(&sade as &dyn Etiket);
let _ = dinamik(&ozel as &dyn Etiket);
// Ayni trait nesnesi, farkli somut tipleri tek bir listede tutabiliyor - statik
// gonderim bunu yapamazdi, cunku bir Vec<T> tek bir somut T'ye kilitlenir.
let liste: Vec<Box<dyn Etiket>> = vec![
Box::new(Sade { defter: Rc::clone(&defter) }),
Box::new(Ozel { defter: Rc::clone(&defter) }),
Box::new(Ozel { defter: Rc::clone(&defter) }),
];
// Dongunun kendisi kac eleman oldugunu biliyor, ama her cagrida hangi govdenin
// kosacagini bilmiyor - bu, elemanin gercek tipine gore ad() icinde cozuluyor.
let adlar: Vec<String> = liste
.iter()
.map(|e| {
say(e.defteri(), "liste-govdesi");
e.ad()
})
.collect();
let mut govdeler: BTreeMap<&str, u32> = BTreeMap::new();
govdeler.insert("statik-gonderim", kere(&defter, "statik-gonderim"));
govdeler.insert("dinamik-gonderim", kere(&defter, "dinamik-gonderim"));
govdeler.insert("liste-govdesi", kere(&defter, "liste-govdesi"));
println!("{:<18}{}", "govde", "kac kez");
for (ad, n) in &govdeler {
println!("{:<18}{}", ad, n);
}
println!("liste-adlari={:?}", adlar);
}
govde kac kez dinamik-gonderim 2 liste-govdesi 3 statik-gonderim 2 liste-adlari=["varsayilan", "ozel", "ozel"]
TR9. statik-gonderim iki kez koştu, dinamik-gonderim de iki kez — birebir aynı sayı.
Fark sayıda değil: iki çağrı biçimi de gövdeyi kaçınılmaz biçimde tetikliyor, ikisi de her
seferinde sınıfına giriyor. Nesneye Dayalı Java kursu kararı hangi tipin verdiğini
ölçmüştü — bildirilen tip mi çalışma zamanı tipi mi. Burada ölçülen o soru değil; ölçülen, iki
gönderim biçiminin gövde sayısını değiştirip değiştirmediği — değiştirmiyor. Sade ve
Ozelin ad yöntemleri kaç kez çağrılırsa çağrılsın, çağrı biçiminin statik ya da dinamik
olması bu sayıyı bir artırmıyor ya da eksiltmiyor.
liste-govdesi satırı bunun pratik sonucunu gösteriyor: Vec<Box<dyn Etiket>>, farklı somut
tipleri (Sade ve iki ayrı Ozel) tek bir koleksiyonda tutabiliyor — Sahiplik ve Bellek
Modeli kursunun Öbekte Tekil Sahiplik dersinde kurulan Box, burada içine bir trait nesnesi
alıyor. statik<T: Etiket> bunu yapamazdı: bir Vec<T> tek bir somut T tipine kilitlidir,
aynı dizi içinde Sade ile Ozeli bir arada tutamaz. Üç elemanlı listenin gövdesi üç kez
koştu — bu da her seferinde sınıfına giriyor, çünkü dönüşüm dizinin her elemanı için
kaçınılmaz. Dikkat edilmesi gereken şey, map içindeki kapanışın kendisinin enin gerçek
tipini hiç bilmemesi: e.ad() çağrısı, listenin üçüncü elemanına ulaşana kadar hangi gövdenin
çalışacağını söylemiyor, bunu yalnız o elemanın taşıdığı yöntem tablosu biliyor. Bu, iki
çağrı biçiminin koşum sayısı eşit olsa da aynı işi görmediğini gösteriyor — trait nesnesi,
çeşitliliği bir koleksiyonun içine taşıyabiliyor, statik gönderim taşıyamıyor.
Nesne Güvenliği
dyn Etiket yazımının derlenebilmesi, Etiketin nesne güvenli olmasına bağlı. Bir trait
nesne güvenli değilse, o trait için bir yöntem tablosu kurulamıyor ve dyn yazımı derleyici
tarafından reddediliyor. İki yaygın engel var: bir yöntem Selfi değer olarak döndürüyorsa
(fn kopya(&self) -> Self gibi), tablo bu yöntemin dönüş boyutunu önceden bilemiyor — çünkü
dyn Etiketin arkasındaki somut tip koşum sırasında değişebiliyor ve her tipin boyutu farklı
olabiliyor. Bir yöntem kendi tip parametresi taşıyorsa (fn cevir<U>(&self, u: U) gibi),
tablo sonsuz sayıda U için ayrı ayrı giriş tutamaz. Bu iki yazım, düzyazıda tarif edildiği
haliyle burada reddediliyor; derlenmedikleri için kod bloğuna konmuyorlar.
Her iki engelin ortak kökeni aynı: yöntem tablosunun her girdisi, çağrı sırasında sabit ve
önceden bilinen boyutta bir işaretçi kümesi olmalı. Selfi değer olarak döndüren bir yöntem
tabloya konursa, çağıran taraf “bu çağrının döndürdüğü değer ne kadar yer kaplayacak”
sorusuna derleme zamanında yanıt bulamaz — çünkü dyn Etiketin arkasında Sade de olabilir,
başka bir boyutta bir tip de. Kendi tip parametresi taşıyan bir yöntem ise tabloya tek bir
giriş olarak sığmaz, çünkü her farklı U için ayrı bir gövde gerekir ve tablo sonsuz sayıda
Uyu önceden listeleyemez. İki engel de aynı ilkeye çıkıyor: trait nesnesi, gerçekleştiren
tipin kimliğini unutmuş bir referanstır; bir yöntem bu unutulan kimliğe (Selfin tam
boyutuna ya da çağıranın seçtiği bir Uya) dayanıyorsa, tablo o yöntemi barındıramaz.
Etiket bu iki engeli de taşımıyor: defteri ve ad ikisi de &self alıyor (değer olarak
Self değil), ikisi de kendi tip parametresi taşımıyor. Bu yüzden nesne güvenli ve
yukarıdaki dinamik fonksiyonu, &dyn Etiket parametresiyle, sorunsuz derleniyor —
kabul edilen tasarımın karşılığı zaten koşturuldu. Bir trait yazarken hangi yöntemlerin
trait nesnesi olarak kullanılabilmesini istediğine karar vermek, o yöntemlerin imzasını
baştan bu iki kurala göre şekillendirmek anlamına geliyor.
Bu koşul, önceki dersteki ilişkili tip kısıtına benzer bir yerde duruyor: ikisi de bir
sözleşmenin her kullanım biçimiyle uyumlu olmayabileceğini gösteriyor. İlişkili tip
kısıtı bir tipin bir sözleşmeyi kaç kez gerçekleştirebileceğini sınırlıyordu; nesne güvenliği
ise bir sözleşmenin hangi çağrı biçimleriyle kullanılabileceğini sınırlıyor. Etiketin
kendisi bu ikisinden de etkilenmiyor çünkü baştan basit tutuldu: hem tek bir ilişkili tipe
ihtiyaç duymuyor hem de nesne güvenli yöntemlerle sınırlı kalıyor. Daha geniş bir sözleşme
yazan bir trait bu iki kısıttan birine ya da ikisine birden çarpabilir; hangisine çarptığı
trait’in imzasından, koşturmadan önce okunabilir.
Örneğin, iki değeri karşılaştırıp büyük olanı değer olarak döndüren bir yöntem —
fn buyuk_olan(&self, oteki: Self) -> Self — nesne güvenli olmazdı: Self burada hem
parametre hem dönüş tipi olarak geçiyor ve dyn Etiketin arkasındaki gerçek tip koşum
sırasında değişebildiği için, tablo bu yöntemin girdi ve çıktı boyutunu önceden sabitleyemez.
Bu yöntem Etikete hiç eklenmedi; eklenseydi &dyn Etiket yazımı derlenmeyecekti. Aynı
karşılaştırma isteği, değeri değil bir referans ya da bir sıralama sonucu (küçük,
eşit, büyük gibi sabit boyutlu bir bilgi) döndürerek nesne güvenli kalabilirdi — bu da bu
kursun genel gözlemiyle örtüşüyor: bir sözleşmenin sınırları, o sözleşmeyi çağıran taraf
değil, gerçekleştirenin unutulmuş kimliğiyle çalışmak zorunda kalan tablo çiziyor.
Sınırlayıcı Ölçüm
Aynı sayı, aynı bedel demek değil. statik-gonderim ile dinamik-gonderimin ikisi de iki
kez koşması, iki çağrı biçiminin çalışma zamanında birbirinin aynı olduğu anlamına
gelmiyor — sayı yalnız gövdenin kaç kez tetiklendiğini ölçüyor, çağrının nasıl
bulunduğunu değil. statik çağrısında hangi gövdenin çalışacağı kaynaktan okunarak
derleme sırasında belirleniyor; dinamik çağrısında aynı bilgi bir yöntem tablosundan
koşum sırasında aranıyor. Bu aramanın kendisi bir adım mı, adımsız mı — bu soru koşum
sayacıyla ölçülemez, çünkü sayaç yalnız say çağrısının kaç kez geçtiğini sayıyor, o
çağrıya ulaşmak için kaç basamak izlendiğini değil. Bu soru zaten Nesneye Dayalı Java
kursunda ele alınmıştı; bu ders onu tekrar sormuyor, yalnız iki biçimin gövde sayısını
eşit bıraktığını gösteriyor.
Bu ayrımın somut bir sonucu var: koşum defterine bakarak “bu kaynak dinamik gönderim mi
kullanıyor, statik mi” sorusuna yanıt verilemez. statik-gonderim ile dinamik-gonderim
satırları yan yana durduğunda, ikisinin de iki olması bir okuyucuyu yanlış bir sonuca
götürebilir — “ikisi aynı şeyi yapıyorsa hangisini seçtiğim önemsiz” sonucuna. Oysa önemli:
liste-govdesi satırının var olabilmesi, yalnız dyn Etiket biçiminin nesne güvenliği
koşulunu sağlamasına bağlıydı; aynı liste statikin kullandığı sınırla hiç kurulamazdı.
Koşum sayısı iki biçimi ayırt etmiyor olsa da, hangi yazımların mümkün olduğu ayrı bir
eksende hâlâ ayrışıyor — bu ders o eksenin sayıyla değil, kodun kendisiyle görüldüğünü
gösteriyor.
Bu kursun ilk dört dersi, bir gövdenin çevresine konan her şeyin — varsayılan gövde, sınır, ilişkili tip, çağrı biçimi — koşum sayısına dokunup dokunmadığını sırayla sordu. Dördünde de yanıt aynı çıktı: dokunmuyor. Gövde nasıl bulunursa bulunsun (bir tip parametresinden ya da bir yöntem tablosundan), çağrıldığı her yerde kaçınılmaz biçimde koşuyor ve bu, üzerine hangi sözdizimi kararının bindirildiğinden bağımsız.
Özet
- Statik gönderim (
<T: Etiket>) derleme zamanında, dinamik gönderim (&dyn Etiket) koşum sırasında bir yöntem tablosundan çözülür. - İki biçim de her seferinde sınıfına giriyor ve koşum sayısı birebir aynı çıkıyor: iki ve iki — çağrı biçimi gövdenin kaç kez tetiklendiğini değiştirmiyor.
- Trait nesnesi (
Box<dyn Etiket>), farklı somut tipleri tek bir koleksiyonda tutabilir; statik gönderim bunu yapamaz çünkü bir tip parametresi tek bir somut tipe kilitlenir. - Nesne güvenliği, bir trait’in
dynile kullanılabilmesi için gereken koşuldur:Selfi değer olarak döndüren ya da kendi tip parametresi taşıyan yöntemler bu koşulu bozar. - Aynı koşum sayısı aynı bedel anlamına gelmez; çağrının nasıl bulunduğu (derleme zamanı mı, koşum zamanı mı) koşum sayacıyla ölçülemeyen ayrı bir sorudur.
Sonraki Adım
Bu derste gövdenin kendisi hep elle yazılıydı — ad yönteminin gövdesi ya trait’in
varsayılanından ya da Ozelin kendi yazdığından geliyordu. Bir gövde hiç elle yazılmadan da
üretilebilir. Sıradaki ders, kaynakta hiçbir satırı görünmeyen bir gövdenin koşum sırasında
nasıl çalıştığını ve bu çalışmanın neden koşum sayacıyla sayılamadığını gösteriyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.