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.