Ders 05 / 15
Türetilen Trait'ler
Bir öznitelik satırı, bir trait'i elle yazmadan gerçekleştirebilir — türetme; elle yazılan gövde koşum defterine bir sayaç çağrısı koyabilirken, türetilmiş gövde kaynakta hiç yazılı olmadığı için sayılamaz ve yalnız davranışıyla gözlemlenebilir; üstelik aynı türetme iki farklı veri düzeninde iki farklı sonuç verir.
Yayın Güncelleme
Bu derste
- Türetme özniteliğinin bir trait'i elle yazmadan gerçekleştirdiğini gösterir
- Elle yazılan bir gövdenin koşum sayısının sayılabildiğini, türetilmiş bir gövdenin sayılamadığını karşılaştırarak açıklar
- Aynı türetmenin farklı veri düzenlerinde farklı sonuçlar verebildiğini koşturarak gösterir
İçindekiler
Önceki dört ders elle yazılan gövdeleri ölçtü: bir trait’in varsayılan gövdesi, sınırlı bir
genel gövde, ilişkili tip taşıyan bir gövde, statik ya da dinamik çağrılan bir gövde — hepsi
kaynakta bir yerde, bir fn satırının altında duruyordu. Bir trait gerçekleştirimi bazen hiç
elle yazılmaz: #[derive(...)] öznitelik satırı, derleyiciye “bu trait’i bu tip için sen
üret” der ve gövdeyi kaynağa hiç yazmadan ortaya çıkarır. Bu ders bu türden bir gövdenin
kaç kez koştuğunu soruyor — ve bu kez soruya tam bir sayıyla yanıt verilemediğini gösteriyor.
Elle Yazılan ile Türetilmiş Arasındaki Fark
Bir tip için eşitlik karşılaştırması iki yolla kurulabilir: impl PartialEq for Tip bloğunu
elle yazarak, ya da #[derive(PartialEq)] satırını tipin üstüne koyarak. İkisi de aynı
sonucu üretir — iki değerin alan alan eşit olup olmadığını söyler — ama kaynaktaki
karşılıkları farklı.
// kere.rs — kesit: say/kere defteri, elle yazilan esitlik govdesi.
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)
}
// Elle yazilan gerceklestirim: govde kaynakta duruyor, sayilabiliyor.
pub struct ElleEsit {
pub x: i32,
pub y: i32,
pub defter: Defter,
}
impl PartialEq for ElleEsit {
fn eq(&self, oteki: &Self) -> bool {
say(&self.defter, "elle-esitlik-govdesi");
self.x == oteki.x && self.y == oteki.y
}
}
// Turetilmis gerceklestirim: govde kaynakta yok, derleyici alan alan karsilastirma uretiyor.
#[derive(PartialEq)]
pub struct TuretilmisEsit {
pub x: i32,
pub y: i32,
}
ElleEsitin eq yöntemi, kaynakta iki satırlık bir gövde taşıyor: önce bir sayma çağrısı,
sonra alan alan karşılaştırma. TuretilmisEsit ise hiçbir impl PartialEq bloğu taşımıyor —
tek satırı #[derive(PartialEq)] ve bu satır bir yöntem gövdesi değil, derleyiciye bir
talimat. Derleyici bu talimatı okuyunca kendi eq gövdesini üretiyor; bu gövde
ElleEsitinkiyle aynı işi yapıyor (x alanlarını ve y alanlarını karşılaştırmak) ama
kaynakta hiçbir satırda görünmüyor.
Tek bir #[derive(...)] satırı birden fazla trait’i aynı anda üretebilir; parantez içine
virgülle ayrılmış trait adları yazılır. Bu kursta zaten görülen Clone da bu yolla
türetilebilir, Debug (bir değeri okunabilir bir metin kalıbına döken trait) da:
// turetme-debug.rs — Debug turetmesi, alanlari otomatik bir metin kalibina donusturur.
#[derive(Debug, Clone, PartialEq)]
struct Nokta {
x: i32,
y: i32,
}
fn main() {
let a = Nokta { x: 3, y: 5 };
let b = a.clone();
println!("{:?}", a);
println!("esit_mi={}", a == b);
}
Nokta { x: 3, y: 5 }
esit_mi=true
Üç trait de (Debug, Clone, PartialEq) tek satırda türetildi ve üçünün de gövdesi
kaynakta görünmüyor. {:?} çıktısı, Debugin ürettiği gövdenin alan adlarını ve
değerlerini kendiliğinden bir araya getirdiğini gösteriyor — bu kalıp da elle yazılmadı, tıpkı
eq gibi derleyici tarafından üretildi. Bu üç trait birlikte türetilebiliyor çünkü üçü de
aynı ilkeye dayanıyor: yapının her alanı için aynı işlemi (yazdırmak, kopyalamak,
karşılaştırmak) sırayla uygulamak. Bu ortak ilke, ilerleyen bir derste (dizileştirme)
tekrar karşımıza çıkacak.
Sayılabilen ve Sayılamayan
// turetme-sayilamaz.rs — elle yazilan govde sayilir, turetilmis govde sayilamaz.
mod kere;
use kere::{kere, Defter, ElleEsit, TuretilmisEsit};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
let a = ElleEsit { x: 1, y: 2, defter: Rc::clone(&defter) };
let b = ElleEsit { x: 1, y: 2, defter: Rc::clone(&defter) };
let c = ElleEsit { x: 9, y: 9, defter: Rc::clone(&defter) };
let esit_mi = a == b;
let esit_degil_mi = a == c;
println!("elle: esit_mi={} esit_degil_mi={}", esit_mi, esit_degil_mi);
println!("elle-esitlik-govdesi={}", kere(&defter, "elle-esitlik-govdesi"));
// Turetilmis PartialEq'in govdesi kaynakta yazili degil - bu yuzden bir sayma
// cagrisi icine koyamayiz. Yalnizca davranisini gozlemleyebiliriz.
let p = TuretilmisEsit { x: 1, y: 2 };
let q = TuretilmisEsit { x: 1, y: 2 };
let r = TuretilmisEsit { x: 9, y: 9 };
println!("turetilmis: esit_mi={} esit_degil_mi={}", p == q, p == r);
}
elle: esit_mi=true esit_degil_mi=false elle-esitlik-govdesi=2 turetilmis: esit_mi=true esit_degil_mi=false
TR10. elle-esitlik-govdesi iki kez koştu — a == b ve a == c çağrılarının her ikisi
de eq gövdesini tetikledi, çünkü gövdenin içine bir sayma çağrısı elle yerleştirildi. Bu
satır her seferinde sınıfına giriyor: == işleci her kullanıldığında kaçınılmaz biçimde
çalışıyor. TuretilmisEsit için aynı satırı yazamıyoruz — derleyicinin ürettiği gövdenin
içine hiçbir kaynak satırı ekleyemediğimiz için, kaç kez çalıştığını gösteren bir sayaç
koyamıyoruz. p == q ve p == r çağrılarının ikisinin de doğru sonucu verdiğini
görebiliyoruz (davranış gözlemi), ama bu iki çağrının arkasındaki gövdenin kaç kez
tetiklendiğini koşum defterine yazdıramıyoruz. Bu, kaynakta yazılı olmayan bir gövdenin
kursun üç sınıfından hiçbirine kesin biçimde yerleştirilemediğini gösteriyor — çalıştığı
kesin (çağrı her yapıldığında bir sonuç dönüyor), ama koşum sayısı bu yöntemle ölçülemez.
Nesneye Dayalı Java kursu, kayıt (record) tipinin eşitlik ve metin gösterimini otomatik kurduğunu göstermişti — orada ölçülen kararı hangi tipin verdiğiydi, kaydın kendi ürettiği gövdenin çalışıp çalışmadığı değil. Bu ders benzer bir otomatik üretime bakıyor, ama farklı bir soru soruyor: üretilen bu gövde, kursun saymayı esas alan yöntemine göre sayılabilir mi? Yanıt hayır — ders bunu açıkça söylüyor, bir sayı uydurmuyor.
Türetilmiş bir gövdenin sayılamaması, onun rastgele ya da öngörülemez olduğu anlamına
gelmiyor — tam tersine, #[derive(PartialEq)]in ne üreteceği tamamen belirlenimci: yapının
alanlarını bildirim sırasıyla tek tek karşılaştırır, hepsi eşitse true, biri bile
eşitsizse false döner. Bu kural her #[derive(PartialEq)] için aynıdır ve yapı
bildirimine bakarak koşturmadan bile önceden söylenebilir. Sayılamayan şey davranışın
kendisi değil, o davranışın çalışma zamanındaki çağrı sayısı: kaç kez eq çalıştığını
görmek istiyorsak koşum defterine bir satır eklememiz gerekiyor ve türetilmiş kaynakta
eklenecek bir satır yok.
“Gövde kaynakta yok” demek, “hiçbir gövde derlenmiyor” demek değil. #[derive(...)]
satırı, derleme sırasında impl bloğuna benzer bir kod parçası üretiyor ve bu üretilmiş
kod, elle yazılmış bir impl bloğuyla aynı yolla derleniyor — üretim, derlemenin geri
kalanından önce, ayrı bir aşamada oluyor. Yazan kişinin gördüğü kaynakta bu üretilmiş kod
hiç görünmüyor; yalnız #[derive(PartialEq)] satırı görünüyor, ürettiği onlarca satırlık
karşılaştırma mantığı görünmüyor. Bu ders “yazılmayan bir gövde de koşar” derken tam bunu
kastediyor: koşan şey var, gerçekten çalışıyor, ama yazan kişinin okuduğu kaynakta bir
karşılığı yok. Sayaç çağrısı ekleyemememizin nedeni de bu — sayaç eklemek için önce
üzerine ekleme yapılacak bir kaynak satırına ihtiyaç var ve türetilmiş gövdede öyle bir
satır elimizde değil.
Aynı Türetme, Farklı Veri Düzeni, Farklı Sonuç
Türetmenin kaynakta gövdesi olmaması, her tipte aynı anlama geldiği izlenimini
uyandırabilir. Değil: aynı #[derive(Clone)] satırı, iki farklı veri düzeninde iki farklı
davranış üretiyor. derive her zaman “alan alan aynı işlemi uygula” der, ama “aynı işlem”in
ne anlama geldiği alanın kendi tipine bağlı — bir i32 için kopyalamak yeni bir sayı
yazmaktır, bir Rc<...> için kopyalamak sayaç artırıp aynı hücreyi paylaşmaktır.
// turetme-duzen.rs — ayni turetme, iki veri duzeninde iki farkli sonuc verir.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone)]
struct Bagli {
veri: Rc<RefCell<i32>>,
}
#[derive(Clone)]
struct Ayrik {
veri: i32,
}
fn main() {
let a = Bagli { veri: Rc::new(RefCell::new(1)) };
let b = a.clone();
*a.veri.borrow_mut() += 41;
println!("bagli klon paylasiyor mu: {}", *b.veri.borrow() == 42);
let c = Ayrik { veri: 1 };
let mut d = c.clone();
d.veri += 41;
println!("ayrik klon paylasiyor mu: {}", c.veri == d.veri);
}
bagli klon paylasiyor mu: true ayrik klon paylasiyor mu: false
TR11. Baglinin klonu, kaynaktaki değeri paylaşıyor: a üzerinden yapılan
değişiklik bde de görünüyor, çünkü Rc<RefCell<i32>> alanının türetilmiş klonu yalnız
sayacı bir artırıp aynı hücreye ikinci bir referans veriyor — Sahiplik ve Bellek Modeli
kursunun Referans Sayımı dersinde kurulan davranış. Ayrikin klonu ise bağımsız: d
üzerinde yapılan değişiklik cyi hiç etkilemiyor, çünkü i32 alanının türetilmiş klonu
kendi başına bir kopya üretiyor. #[derive(Clone)] satırı ikisinde de birebir aynı, ama
sonuç veri düzenine göre ayrışıyor — türetme “alan alan klonla” demenin kısa yolu, alanın
kendisinin ne anlama geldiğine karışmıyor.
Bu ayrım, elle yazılan bir gövdeyle türetilmiş bir gövde arasındaki farkı bir kez daha
gösteriyor: elle yazılan bir impl Clone, isterse Bagli için de bağımsız bir kopya
üretecek biçimde yazılabilirdi — içindeki RefCellin değerini okuyup yeni bir Rc kurarak.
Türetilmiş gövde bu seçeneği sunmuyor; her zaman “alanları olduğu gibi klonla” diyor, alanın
kendi Clone gerçekleştirimi ne yapıyorsa onu miras alıyor. Bir tipin klonunun paylaşımlı mı
bağımsız mı olacağına türetme değil, alan tiplerinin seçimi karar veriyor.
Türetmenin bir de sessiz bir önkoşulu var: bir yapı için #[derive(PartialEq)] yazmak,
yapının her alanının kendi başına PartialEqi gerçekleştirmiş olmasını gerektiriyor —
derleyici alan alan karşılaştırma üretirken her alan için == çağırıyor ve bu çağrının
derlenebilmesi için alanın kendi tipinin karşılaştırılabilir olması şart. Noktanın x ve
y alanları i32 olduğu, i32 de PartialEqi zaten taşıdığı için türetme sorunsuz
derleniyor. Alanlardan biri karşılaştırılamayan bir tip taşısaydı (örneğin bir fonksiyon
işaretçisi), aynı #[derive(PartialEq)] satırı derleyici tarafından reddedilirdi — bu redd
de, kursun genel kuralı gereği, bir kod bloğuna konmuyor; yukarıdaki Nokta ve ElleEsit
örnekleri zaten kabul edilen karşılığı gösteriyor: yalnız karşılaştırılabilir alanlar
taşıyan yapılar.
Sınırlayıcı Ölçüm
Türetme her tipte aynı anlama gelmiyor. Aynı #[derive(Clone)] bir tipte paylaşımlı bir
kopya, başka bir tipte bağımsız bir kopya üretebiliyor; aynı #[derive(PartialEq)] bir
tipte iki alanı, başka bir tipte on alanı karşılaştırabiliyor — üretilen gövdenin ne
yaptığı, üzerine kurulduğu tipin alanlarına bağlı. Bu ders türetilmiş gövdenin kaç kez
koştuğunu sayamadığı gibi, iki farklı türetimin aynı işi yaptığını da varsaymıyor; yalnız
ikisinin de derleyici tarafından üretildiğini ve davranışının koşturularak gözlemlenebildiğini
gösteriyor. Kursun ölçüm yöntemi burada açık bir sınıra çarpıyor: kaynakta bir sayma çağrısı
yazılamayan her yerde, sayı yerine davranış karşılaştırması devreye giriyor.
Bu sınırın pratik bir sonucu var: türetilmiş bir gövdeye güvenmek, o gövdenin tasarımını
anlamayı gerektiriyor, çünkü çalışma zamanında onu izlemenin bir yolu yok. Bagli tipindeki
Rc<RefCell<i32>> alanı için #[derive(Clone)] yazmak, “bu tipin kopyası paylaşımlı olsun”
kararını görünmez biçimde alıyor — bu karar yapı bildirimini okuyan birine derive
satırından değil, alanın kendi tipinden belli oluyor. Elle yazılan bir impl Clone bu kararı
gövdenin içinde açıkça gösterebilirdi (bir referans mı kopyalanıyor, bir değer mi); türetilmiş
gövde bu kararı alan tipine devrediyor ve kaynakta ayrıca yazmıyor. Bu, türetmenin kısayolunun
bedeli: kaynak kısalıyor, ama kaynağı okuyanın davranışı anlamak için alan tiplerine kadar
inmesi gerekiyor.
Önceki dört ders, gövde her zaman kaynakta bir fn satırının altında durduğu için, koşum
sayısını say çağrısıyla doğrudan ölçebiliyordu. Bu ders o varsayımı kırıyor: gövde kaynakta
yok, ama davranış var ve doğrulanabilir. Kursun ölçüm yöntemi, sayamadığı yerde susmayı
seçiyor — bir sayı uydurmak yerine, “burada sayı yok” diyor.
Özet
- Türetme özniteliği (
#[derive(...)]), bir trait’i elle yazmadan, derleyiciye ürettirerek gerçekleştirir; kaynakta bir yöntem gövdesi bırakmaz. - Elle yazılan gövde (
elle-esitlik-govdesi) her seferinde sınıfına girer ve sayılabilir — bu derste iki kez koştu; türetilmiş gövde her çağrıldığında çalışır ama kaynakta gövdesi olmadığı için bu koşum sayılamaz. - Sayılamayan bir gövdenin davranışı yine de koşturularak gözlemlenebilir: doğru sonucu verip vermediği görülebilir, kaç kez çalıştığı görülemez.
- Aynı türetme iki farklı veri düzeninde iki farklı sonuç verebilir —
Rc<RefCell<...>>alanı olan bir tipte paylaşımlı, düz bir sayısal alanda bağımsız bir kopya üretir.
Sonraki Adım
Bu ders, bir gövdenin hiç yazılmamış olsa bile çalıştığını gösterdi. Kursun son dersi
başka bir görünmezliğe bakıyor: bir işlecin (+ gibi) çağrıldığı yer kaynakta hiç görünmez,
yalnız işlecin kendisi görünür. Sıradaki ders bu görünmez çağrının kaç kez koştuğunu, üç
terimli bir toplamı koşturarak sayıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.