İçeriğe geç
academia.sh

Ders 03 / 15

İlişkili Tipler

Bir trait, tip parametresi almadan kendi içinde bir tip taşıyabilir — ilişkili tip; aynı sözleşme ilişkili tiple ve genellik parametresiyle yazıldığında koşum sayısı birebir aynı çıkar, fark çağıranın imzada ne yazmak zorunda kaldığındadır ve ilişkili tip bir tip için yalnız tek bir gerçekleştirime izin veren bir kısıt taşır.

Yayın Güncelleme

Bu derste

  • İlişkili tip sözdizimini tanımlar ve bunu aynı sözleşmenin genellik parametresiyle yazılmış biçiminden ayırır
  • İki yazımın koşum sayısının aynı çıktığını, farkın çağıranın yazmak zorunda kaldığı bilgide olduğunu gösterir
  • İlişkili tipin bir tip için tek gerçekleştirime izin veren bir kısıt olduğunu, genellik parametresinin buna izin vermediğini karşılaştırarak açıklar
İçindekiler

Önceki ders bir trait sınırının bir tip parametresini dıştan nasıl kısıtladığını gösterdi: T: Etiket yazımı, çağıranın hangi tipleri geçirebileceğini söylüyordu. Bir trait bazen tersini de yapabilir — sözleşmenin kendi içinde bir tip taşıyabilir, bu tipi çağıran değil gerçekleştiren belirler. Kursun ilk dersinde koşan çekirdekte buna zaten bir örnek vardı: Add trait’inin gerçekleştirimi type Output = Uzunluk; satırını taşıyordu — işleç bölümünün bu satırı, tam olarak bu dersin konusu olan ilişkili tiptir. O ders bunu tanıtmadan geçti; bu ders ona geri dönüyor.

Bir trait sınırı çağırana “bu fonksiyona hangi tipleri geçirebilirsin” sorusunu yanıtlıyordu. İlişkili tip farklı bir soruyu yanıtlıyor: “bu sözleşmeyi gerçekleştiren bir tip, kendisiyle birlikte hangi başka tipi taşır.” Bir trait yalnız yöntem imzaları değil, o yöntemlerin işlediği yardımcı bir tip de bildirebiliyor — bu yardımcı tip her gerçekleştirimde farklı olabilir, ama tek bir gerçekleştirim için sabittir. Bu, bir sözleşmenin yalnız “ne yapılacağını” değil, “hangi tiple çalışılacağını” da tanımlayabildiği anlamına geliyor.

Bu derste ölçülen iki gövde de kursun üç sınıfından yalnız birine giriyor: her seferinde. Ne ilişkili tip ne genellik parametresi, bir gövdenin ertelenmesine ya da atlanmasına yol açıyor — ikisi de yalnız gövdenin imzasını biçimlendiriyor, gövdenin çağrıldığında koşup koşmayacağını değil. Bu, önceki iki dersle aynı gözlem: sınıf, gövdenin nasıl adlandırıldığına ya da nereye bağlandığına değil, çağrıldığında ne olduğuna bakılarak belirleniyor.

İlişkili Tip Nedir

Bir trait bildiriminin içinde type Ad; yazımı, “bu sözleşmeyi gerçekleştiren her tip, kendi içinde bir Ad tipi de belirlemeli” demektir. Gerçekleştirme sırasında bu boşluk type Ad = SomutTip; satırıyla doldurulur. Aşağıdaki iki trait aynı işi iki farklı yazımla tanımlıyor:

// kere.rs — kesit: say/kere defteri, iliskili-tip ve genellik-parametresi sozlesmeleri.

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)
}

// Sozlesme kendi icinde bir tip tasiyor: Cikti, gerceklestiren tarafindan belirlenir.
pub trait Sozlesme {
    type Cikti;
    fn uret(&self, defter: &Defter) -> Self::Cikti;
}

// Ayni fikir, genellik parametresiyle: C caginin disaridan verdigi bir tip.
pub trait SozlesmeGenel<C> {
    fn uret_genel(&self, defter: &Defter) -> C;
}

pub struct Kutu {
    pub deger: i32,
}

impl Sozlesme for Kutu {
    type Cikti = i32;
    fn uret(&self, defter: &Defter) -> i32 {
        say(defter, "iliskili-tip-govdesi");
        self.deger
    }
}

impl SozlesmeGenel<i32> for Kutu {
    fn uret_genel(&self, defter: &Defter) -> i32 {
        say(defter, "genel-parametre-govdesi");
        self.deger
    }
}

Self::Cikti yazımındaki Self:: öneki gelişigüzel değil: trait bildirimi yazılırken henüz hangi tipin bu sözleşmeyi gerçekleştireceği bilinmiyor, bu yüzden Ciktiye çıplak bir ad olarak değil, “gerçekleştiren tipin kendi Ciktisi” anlamına gelen Self::Cikti olarak yazılması gerekiyor. impl bloğunun içinde bu belirsizlik ortadan kalkıyor — Self artık Kutuya karşılık geldiği için type Cikti = i32; satırı doğrudan i32 yazabiliyor, Self:: önekine gerek kalmıyor. Aynı isim (Cikti), trait bildiriminde bir yer tutucu, gerçekleştirim içinde bir karar olarak iki farklı rolde görünüyor.

Sozlesmede Cikti sözleşmenin bir parçası: uret yönteminin dönüş tipi Self::Cikti diye yazılıyor, somut bir tip adı değil. Kutu bu sözleşmeyi gerçekleştirirken Ciktinin i32 olduğunu bir kez, impl bloğunun başında bildiriyor; bloğun geri kalanı bu kararı miras alıyor. SozlesmeGenel<C>de ise aynı bilgi trait’in adının içinde: Kutu için impl SozlesmeGenel<i32> for Kutu yazıldığında C orada i32ye çözülüyor, ama bu bilgi trait adının parçası olduğu için birden fazla kez, farklı C değerleriyle tekrarlanabilir — aşağıda bu farkın kısıt tarafına bakılacak.

Aynı Sayı, Farklı Yazım

İki yazımı da aynı sayıda çağırıp koşum sayısını karşılaştıralım.

// iliskili-tip.rs — ayni is iki yazimla: iliskili tip ve genellik parametresi.
mod kere;
use kere::{kere, Defter, Kutu, Sozlesme, SozlesmeGenel};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let kutu = Kutu { deger: 9 };

    // Cagiran taraf, iliskili tipte Cikti'yi hic yazmiyor - derleyici kendisi biliyor.
    let _ = kutu.uret(&defter);
    let _ = kutu.uret(&defter);
    let _ = kutu.uret(&defter);

    // Genellik parametresinde de ayni sayida cagri, ayni govde.
    let _ = SozlesmeGenel::<i32>::uret_genel(&kutu, &defter);
    let _ = SozlesmeGenel::<i32>::uret_genel(&kutu, &defter);
    let _ = SozlesmeGenel::<i32>::uret_genel(&kutu, &defter);

    let mut govdeler: BTreeMap<&str, u32> = BTreeMap::new();
    govdeler.insert("iliskili-tip-govdesi", kere(&defter, "iliskili-tip-govdesi"));
    govdeler.insert("genel-parametre-govdesi", kere(&defter, "genel-parametre-govdesi"));

    println!("{:<24}{}", "govde", "kac kez");
    for (ad, n) in &govdeler {
        println!("{:<24}{}", ad, n);
    }
}
govde                   kac kez
genel-parametre-govdesi 3
iliskili-tip-govdesi    3

TR5. İkisi de üç kez koştu — her seferinde sınıfına giren iki satır, birebir aynı sayıda. Fark koşum defterinde değil, çağrı satırının kendisinde görünüyor: kutu.uret(&defter) yazımı Ciktinin ne olduğunu hiç söylemiyor, çünkü derleyici bunu Kutunun Sozlesme gerçekleştiriminden zaten biliyor. SozlesmeGenel::<i32>::uret_genel(&kutu, &defter) yazımı ise i32yi açıkça taşıyor — Kutu üzerinde SozlesmeGenelin birden fazla gerçekleştirimi olabileceği için (aşağıda gösteriliyor), hangisinin çağrıldığını belirtmek çağıranın işi haline geliyor.

Arayüzler, Hatalar ve Genellikler kursu Go’nun genelliklerini bağ ekseninden yazmıştı; burada ölçülen o kursun sorduğu soru değil, iki yazımın gövde sayısı — ve sayı birebir eşit çıkıyor. Bu, ilişkili tip ile genellik parametresinin çalışma zamanında farklı bir maliyet taşımadığını gösteriyor; aralarındaki fark tamamen kaynağın okunuşunda ve çağıranın yazmak zorunda kaldığı bilgide.

İlişkili Tip Tip Başınadır

Kısıt, trait’in tamamı için değil, her gerçekleştirim için ayrı ayrı geçerli. Kutu için Cikti bir kez i32ye sabitlendi, ama başka bir tip aynı Sozlesmeyi kendi Ciktisiyle gerçekleştirebilir — kısıtlanan şey “bu trait her yerde tek bir Cikti kullanmalı” değil, “bu tip, bu trait için tek bir Cikti seçmiş olmalı.”

// iki-tip.rs — iliskili tip trait basina degil, gerceklestirim basina sabittir.
mod kere;
use kere::{kere, Defter, Kutu, Sozlesme, SozlesmeGenel};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

struct Etiketli {
    ad: String,
}

impl Sozlesme for Etiketli {
    type Cikti = String;
    fn uret(&self, defter: &Defter) -> String {
        kere::say(defter, "etiketli-govdesi");
        self.ad.clone()
    }
}

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let kutu = Kutu { deger: 5 };
    let etiketli = Etiketli { ad: String::from("baslik") };

    let sayisal: i32 = kutu.uret(&defter);
    let metinsel: String = etiketli.uret(&defter);
    let _ = SozlesmeGenel::<i32>::uret_genel(&kutu, &defter);

    println!("sayisal={} metinsel={}", sayisal, metinsel);
    println!("iliskili-tip-govdesi={}", kere(&defter, "iliskili-tip-govdesi"));
    println!("etiketli-govdesi={}", kere(&defter, "etiketli-govdesi"));
}
sayisal=5 metinsel=baslik
iliskili-tip-govdesi=1
etiketli-govdesi=1

TR8. Kutu Cikti = i32 ile, Etiketli Cikti = String ile aynı Sozlesmeyi gerçekleştiriyor ve ikisi de aynı kaynakta yan yana duruyor — bu, trait’in kendisinin bir tipe bağlı olmadığını, yalnız o tipin gerçekleştirimine bağlı olduğunu gösteriyor. Kısıt hâlâ geçerli: ne Kutu ne Etiketli Sozlesmeyi ikinci bir Cikti ile bir daha gerçekleştiremez. Örnek ayrıca Kutu üzerinde SozlesmeGenel<i32>i de bir kez daha çağırıyor — iki ayrı sözleşmenin (Sozlesme ve SozlesmeGenel) aynı kaynakta ve aynı tip üzerinde bir arada durabildiğini, birbirini dışlamadığını göstermek için.

Genel Kod İçinde Kullanmak

Fark, tek bir somut tip (Kutu) üzerinde çağrı yaparken küçük görünüyor: ikisi de üç kelime ekleyip ekleme farkı kadar kısa. Fark, Sozlesmeyi ya da SozlesmeGeneli başka bir genel fonksiyonun sınırında kullanmaya çalışınca büyüyor.

// genel-kullan.rs — Sozlesme'yi bir baska genel fonksiyonun sinirinda kullanmak.
mod kere;
use kere::{kere, say, Defter, Kutu, Sozlesme, SozlesmeGenel};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

// Cikti tipini ayrica yazmaya gerek yok - T::Cikti derleyici tarafindan cozuluyor.
fn kullan<T: Sozlesme>(x: &T, defter: &Defter) -> T::Cikti {
    say(defter, "genel-kullan-govdesi");
    x.uret(defter)
}

// Ayni fikir genellik parametresiyle: C'yi imzaya AYRICA eklemek gerekiyor.
fn kullan_genel<T: SozlesmeGenel<C>, C>(x: &T, defter: &Defter) -> C {
    say(defter, "genel-kullan-genel-govdesi");
    x.uret_genel(defter)
}

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let kutu = Kutu { deger: 9 };

    let a = kullan(&kutu, &defter);
    let b = kullan_genel(&kutu, &defter);

    println!("kullan={} kullan_genel={}", a, b);
    println!("genel-kullan-govdesi={}", kere(&defter, "genel-kullan-govdesi"));
    println!("genel-kullan-genel-govdesi={}", kere(&defter, "genel-kullan-genel-govdesi"));
}
kullan=9 kullan_genel=9
genel-kullan-govdesi=1
genel-kullan-genel-govdesi=1

TR7. kullan<T: Sozlesme>in dönüş tipi T::Cikti — T neye çözülürse çözülsün, onun Ciktisi otomatik olarak devralınıyor; imzada Cikti için ayrı bir yer yok, çünkü olması gerekmiyor. kullan_genel ise sınırında T: SozlesmeGenel<C> yazdıktan sonra Cyi fonksiyonun kendi tip parametreleri listesine de eklemek zorunda kalıyor — C Tden otomatik olarak çıkarılamıyor, çünkü T teorik olarak SozlesmeGenelin birden fazla gerçekleştirimini taşıyabilir ve derleyici hangisinin kastedildiğini bilemez. Bu, bir önceki bölümdeki çağrı yerindeki farkın, genel kod yazarken bir imza farkına dönüştüğünü gösteriyor: kullanin imzası tek tip parametresi taşırken kullan_genelinki iki taşıyor, ikinci fonksiyonun tek işi ilkiyle aynı olsa bile.

Trait Sınırları dersindeki birlestir<T: Etiket, U: Etiket> örneği iki bağımsız tip parametresinin nasıl bir arada durabildiğini göstermişti; kullan_genel<T: SozlesmeGenel<C>, C> aynı yazımı kullanıyor, ama burada C bağımsız bir seçim değil, Tnin zaten taşıdığı bir bilginin tekrarı. İki fonksiyon da sözdizimsel olarak aynı kalıba uyuyor — <...> içinde birden fazla parametre — ama birlestirde her parametre gerçekten farklı bir soruyu yanıtlarken, kullan_genelde C yalnız Tnin taşıdığı bir bilgiyi çağıranın yeniden yazmasını istiyor. İlişkili tip bu tekrarı ortadan kaldırıyor.

Sınırlayıcı Ölçüm

İlişkili tip, sözleşmeye bir kısıt ekliyor: bir tip, Sozlesmeyi yalnız bir kez gerçekleştirebilir, çünkü Cikti o tip için tek bir değere bağlanıyor. Kutu için Cikti = i32 bir kez bildirildikten sonra, aynı tip için ikinci bir impl Sozlesme for Kutu bloğu — Ciktiyi String yapmaya çalışan bir ikincisi — derleyici tarafından reddedilir; bu redd kod bloğuna konmuyor, çünkü derlenmeyen kaynak bu depoda hiçbir zaman kod bloğu olarak gösterilmiyor. Kısıtın kendisi düzyazıyla tarif ediliyor; kabul edilen karşılığı yukarıdaki iliskili-tip.rs zaten koşturdu — Kutu için tek bir Sozlesme gerçekleştirimi var ve kaynak bu haliyle derleniyor.

SozlesmeGenel<C> aynı kısıtı taşımıyor: C trait adının bir parçası olduğu için, SozlesmeGenel<i32> ile SozlesmeGenel<String> derleyici için iki ayrı traittir ve Kutu ikisini de aynı anda gerçekleştirebilir.

// kisit.rs — genellik parametresi ayni tip icin birden fazla gerceklestirime izin verir;
// iliskili tip vermez (bir tip = bir Cikti baglamasi).
trait SozlesmeGenel<C> {
    fn uret_genel(&self) -> C;
}

struct Kutu {
    deger: i32,
}

impl SozlesmeGenel<i32> for Kutu {
    fn uret_genel(&self) -> i32 {
        self.deger
    }
}

impl SozlesmeGenel<String> for Kutu {
    fn uret_genel(&self) -> String {
        self.deger.to_string()
    }
}

fn main() {
    let kutu = Kutu { deger: 9 };
    let sayisal: i32 = SozlesmeGenel::<i32>::uret_genel(&kutu);
    let metinsel: String = SozlesmeGenel::<String>::uret_genel(&kutu);
    println!("sayisal={} metinsel={}", sayisal, metinsel);
}
sayisal=9 metinsel=9

TR6. Kutu, SozlesmeGenel<i32> ve SozlesmeGenel<String>i aynı anda gerçekleştiriyor — ikisi de kabul edildi, ikisi de derlendi. Bu, ilişkili tipin sunduğu kısıtın bir eksiklik değil bilinçli bir tasarım tercihi olduğunu gösteriyor: Sozlesme kullanılsaydı Kutunun Ciktisi tek bir değere sabitlenir, çağıran taraf hiçbir zaman “hangi Cikti?” diye sormak zorunda kalmazdı. SozlesmeGenel kullanıldığında bu esneklik kazanılıyor, ama bedeli çağrı yerinde görünüyor: kutu.uret_genel(&defter) yazımı Kutu iki gerçekleştirimi barındırdığı için tek başına yeterli değil, hangi C kastedildiğinin (::<i32> ya da ::<String>) açıkça yazılması gerekiyor. İlişkili tipte bu belirtme hiç gerekmiyor, çünkü zaten yalnız bir seçenek var.

Bu kısıt bir eksiklik olarak değil, bir tasarım aracı olarak okunmalı. Bir sözleşme, bir tipin doğası gereği tek bir çıktı tipiyle çalıştığını öngörüyorsa (örneğin bir kap için “içindeki eleman tipi” ya da bir dönüştürücü için “ürettiği sonuç tipi”) ilişkili tip bu tekliği bir sözdizimi kuralına dönüştürür: yazan taraf yanlışlıkla aynı tipi iki farklı çıktı tipiyle gerçekleştiremez, çünkü derleyici buna izin vermiyor. Genellik parametresi ise bu tekliği zorunlu kılmaz — bir tipin gerçekten birden fazla çıktı biçimiyle çalışabileceği durumlarda (bu derste görüldüğü gibi, bir Kutunun hem sayısal hem metinsel bir karşılığı olabilir) doğru araç odur. İki yazımın da var olmasının nedeni, “bir tip = bir çıktı” ile “bir tip = birden fazla çıktı” sorularının farklı sözleşmeler gerektirmesi.

Özet

  • İlişkili tip, bir trait’in kendi içinde bir type Ad; bildirimiyle taşıdığı, gerçekleştiren tarafından doldurulan bir tip yuvasıdır; genellik parametresi aynı bilgiyi trait adının içinde taşır.
  • İki yazım da her seferinde sınıfına giriyor ve koşum sayısı birebir aynı çıkıyor (üç ve üç); fark çalışma zamanında değil, çağıranın imzada yazmak zorunda kaldığı bilgide.
  • İlişkili tip bir kısıt taşır: bir tip, bir trait’i ilişkili tipiyle yalnız bir kez gerçekleştirebilir.
  • Genellik parametresi bu kısıtı taşımaz: aynı tip, aynı trait’i farklı parametre değerleriyle birden fazla kez gerçekleştirebilir — bedeli, çağıranın hangi gerçekleştirimi kastettiğini açıkça belirtmek zorunda kalmasıdır.

Sonraki Adım

Bu iki ders, aynı gövdenin sınır ya da ilişkili tiple yazıldığında koşum sayısının değişmediğini gösterdi — ikisi de derleme zamanında çözülüyor. Sıradaki ders çağrının ne zaman çözüldüğü sorusuna dönüyor: aynı sözleşme, derleme zamanında çözülen bir çağrıyla mı, koşum sırasında çözülen bir çağrıyla mı kullanılıyor ve bu ikisi koşum sayısını değiştiriyor mu?

İ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