İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat