İçeriğe geç
academia.sh

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 dyn ile 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat