İçeriğe geç
academia.sh

Ders 06 / 15

İşleç Aşırı Yükleme

Bir işlecin (+ gibi) her kullanımı, arkasında görünmeyen bir trait çağrısıdır; üç terimli bir toplam iki kez çağırır, beş terimli bir toplam dört kez — işlecin kısa sözdizimi çağrı sayısını değiştirmez, yalnız çağrının kaynakta görünür olup olmadığını değiştirir ve bu sayı ancak koşum defteriyle bilinebilir.

Yayın Güncelleme

Bu derste

  • Bir işlecin kullanımının, standart kitaplıktaki bir trait'in yöntem çağrısına karşılık geldiğini gösterir
  • Zincirlenmiş bir işleç ifadesinde çağrı sayısının terim sayısından bir eksik olduğunu koşturarak doğrular
  • İşlecin çağrı yerinin kaynakta görünmediğini, bu yüzden çağrı sayısının yalnız sayılarak bilinebildiğini açıklar
İçindekiler

Önceki ders, kaynakta hiç yazılı olmayan bir gövdenin yine de çalıştığını gösterdi. Bu ders tam tersi bir görünmezliğe bakıyor: gövde kaynakta yazılı, ama onu çağıran satır kaynakta görünmüyor. a + b yazıldığında bir çağrı yapılıyor, ama kaynakta add adını taşıyan hiçbir satır yok — çağrı, + işlecinin arkasına gizlenmiş. Nesneye Dayalı Java kursu işleç aşırı yüklemeyi zaten tanımlamıştı; orada ölçülen kararı hangi tipin verdiğiydi, burada ölçülen bir işleç gövdesinin kaç kez koştuğu.

Bu kursun beş dersi boyunca ölçülen her gövde kaynakta bir adla anılabiliyordu: ad, bir sınırlı fonksiyon, uret, statik/dinamik, eq. İşleç aşırı yüklemede bu adlandırma kayboluyor — kaynakta yalnız + görünüyor, add sözcüğü hiçbir yerde geçmiyor. Bu son ders, kursun ölçü ekseninin en görünmez uygulamasını taşıyor: bir gövdenin kaç kez koştuğunu saymak için, önce o gövdeyi çağıran satırı kaynakta tanımak gerekiyor ve işleçlerde bu tanıma adım kaynağın kendisinde değil, dilin sözdizimi kuralında.

İşleç Bir Trait Çağrısıdır

Rust’ta + işleci sabit bir davranış değil, std::ops::Add trait’inin bir çağrısı. Bir tip + ile toplanabilir olsun istiyorsa, Addi gerçekleştirmesi gerekiyor; gerçekleştirmezse + derlenmez. a + b yazımı, derleyici tarafından Add::add(a, b)e açılıyor — kaynakta görünen kısayol, arkasında sıradan bir yöntem çağrısı taşıyor.

Bu açılma, önceki dersteki türetmeyle aynı kategoride değil. Türetilmiş bir gövde kaynakta hiç yazılmıyordu; add gövdesi burada tamamen yazılı, impl Add for Uzunluk bloğunun içinde satır satır duruyor. Görünmeyen şey gövde değil, gövdeyi çağıran satır — + işareti bir çağrı taşıdığını söylemiyor, yalnız iki değeri yan yana koyuyor. Bu, temel tamsayı ve kayan noktalı tipler için de aynı: 3 + 4 yazıldığında bile, arka planda aynı kalıp işliyor, yalnız i32 gibi ilkel tipler için bu çağrı derleyicinin kendi yerleşik kurallarıyla, standart kitaplıktaki Add gerçekleştirimi üzerinden çözülüyor. Uzunluk gibi kullanıcı tanımlı bir tip için aynı yolu açmak, tam olarak Addi gerçekleştirmek anlamına geliyor.

// kere.rs — kesit: say/kere defteri, Uzunluk ve Add gerceklestirimi.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ops::Add;
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)
}

#[derive(Clone)]
pub struct Uzunluk {
    pub deger: i32,
    pub defter: Defter,
}

impl Add for Uzunluk {
    type Output = Uzunluk;
    fn add(self, oteki: Uzunluk) -> Uzunluk {
        say(&self.defter, "islec-govdesi");
        Uzunluk {
            deger: self.deger + oteki.deger,
            defter: Rc::clone(&self.defter),
        }
    }
}

impl Add for Uzunluk bloğu, önceki derste tanıdık bir yazımı taşıyor: type Output = Uzunluk; satırı, İlişkili Tipler dersinde ele alınan ilişkili tip — Addin kendisi, toplamın sonuç tipini kendi içinde bir tip olarak taşıyor. Bu ders Outputa değil, add gövdesinin kaç kez çağrıldığına bakıyor.

add yönteminin imzası da kendi başına bir gözlem taşıyor: fn add(self, oteki: Uzunluk) -> Uzunluk. Parametre selfin kendisi (&self değil) alınıyor — toplama işlemi iki Uzunluk değerini tüketiyor, ödünç almıyor. Bu, Sahiplik ve Bellek Modeli kursunun sahiplik ve taşıma dersinde kurulan kuralın burada tekrar karşımıza çıkması: a + b yazıldığında hem a hem b bu ifadeden sonra kullanılamaz hale geliyor, çünkü ikisi de addin içine taşındı. Yukarıdaki a.clone() + b yazımında anın önce çoğaltılmasının nedeni tam bu — a ikinci toplamda (toplam + a) tekrar kullanılacağı için, ilk toplamda tüketilmemesi gerekiyor.

Üç Terim, İki Çağrı

// islec.rs — bir islecin her kullanimi bir cagridir; uc terimli toplam iki kez cagirir.
mod kere;
use kere::{kere, Defter, Uzunluk};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn olustur(defter: &Defter, deger: i32) -> Uzunluk {
    Uzunluk { deger, defter: Rc::clone(defter) }
}

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let a = olustur(&defter, 3);
    let b = olustur(&defter, 4);

    // Uc terim, iki '+' cagrisi: (a.clone() + b) + a
    let toplam = a.clone() + b;
    let toplam = toplam + a;
    println!(
        "uc-terim-toplam={} islec-govdesi={}",
        toplam.deger,
        kere(&defter, "islec-govdesi")
    );

    // Bes terim, dort '+' cagrisi - ayni ilkeyi ayri bir anahtarla dogruluyor.
    let defter2: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let bes: Vec<Uzunluk> = (1..=5).map(|d| olustur(&defter2, d)).collect();
    let mut uzun_toplam = bes[0].clone();
    for terim in bes.into_iter().skip(1) {
        uzun_toplam = uzun_toplam + terim;
    }
    println!(
        "bes-terim-toplam={} islec-govdesi={}",
        uzun_toplam.deger,
        kere(&defter2, "islec-govdesi")
    );
}
uc-terim-toplam=10 islec-govdesi=2
bes-terim-toplam=15 islec-govdesi=4

TR12. İlk ölçüm üç terimi ((anın bir kopyası, b, anın kendisi) topluyor ve islec-govdesi iki kez koştu. Kaynakta + işareti de iki kez göründü — a.clone() + b ve sonucun a ile toplanması — ve bu görünüm gövdenin kaç kez çağrıldığıyla birebir örtüşüyor. Bu satır her seferinde sınıfına giriyor: işlecin her kullanımı, kaçınılmaz biçimde bir çağrıya karşılık geliyor. İkinci ölçüm aynı ilkeyi ayrı bir defterle doğruluyor: beş terim toplanırken kaynakta bir kez yazılan + dört kez koştu. İki ölçüm birlikte genel kuralı gösteriyor: n terimli bir toplamda işleç gövdesi n eksi bir kez koşar — ilk terim hiçbir çağrıya ihtiyaç duymadan zaten elde var, geri kalan her terim bir çağrı gerektiriyor.

Bu kural, toplamanın kendi yapısından geliyor: iki terim toplamak tek bir çağrı, üçüncü terimi eklemek ikinci bir çağrı, dördüncüyü eklemek üçüncü bir çağrı. Beş terimli döngüde bu birikim bes[0]den başlıyor — henüz hiçbir toplama yapılmadığı için sıfır çağrı — ve her yeni terim bir çağrı ekleyerek dört çağrıda tamamlanıyor. Kaynaktaki döngü gövdesi tek bir satır (uzun_toplam = uzun_toplam + terim;), ama bu satırın kaç kez yazıldığı değil, kaç kez çalıştığı önemli — kaynakta bir kez yazılan bir satır, döngü aracılığıyla kendini dört kez tekrar ediyor. Bu, İşleç Aşırı Yükleme dersinin taşıdığı ek bir gözlem: bir çağrının kaynakta kaç kez yazıldığı ile koşumda kaç kez çalıştığı farklı sayılar olabilir ve döngü bu ikisini ayrıştıran en yaygın araç.

Nesneye Dayalı Java kursu işleç aşırı yüklemenin kararı hangi tipin verdiğini ölçmüştü — bir işlecin hangi gerçekleştirimin çağrılacağını işlenen değerlerin bildirilen tipinin mi çalışma zamanı tipinin mi belirlediğini. Bu kurs o soruyu tekrar sormuyor; burada ölçülen, zaten belirlenmiş bir gerçekleştirimin gövdesinin kaç kez koştuğu. Rust’ta bu belirleme zaten derleme zamanında, Uzunlukin somut tipinden okunarak yapılıyor — kararın kendisi bu dersin konusu değil.

Sözdizimi Kısayoldur, Çağrı Aynı

+ yazımının bir kısayol olduğu, Add::addi açıkça çağırarak da doğrulanabilir — iki yazım aynı çağrıya açılıyorsa, ikisinin de koşum defterine bıraktığı iz aynı olmalı.

// islec-esdeger.rs — '+' yazimi ile Add::add cagrisi ayni sayida kosuyor.
mod kere;
use kere::{kere, Defter, Uzunluk};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ops::Add;
use std::rc::Rc;

fn olustur(defter: &Defter, deger: i32) -> Uzunluk {
    Uzunluk { deger, defter: Rc::clone(defter) }
}

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let a = olustur(&defter, 5);
    let b = olustur(&defter, 6);

    // Iki yazim, ayni cagri: '+' iceren ve Add::add'i acikca cagiran.
    let sozdizimiyle = a.clone() + b.clone();
    let acikca = Add::add(a, b);

    println!("sozdizimiyle={} acikca={}", sozdizimiyle.deger, acikca.deger);
    println!("islec-govdesi={}", kere(&defter, "islec-govdesi"));
}
sozdizimiyle=11 acikca=11
islec-govdesi=2

TR13. a.clone() + b.clone() bir çağrı, Add::add(a, b) bir çağrı — ikisi de aynı add gövdesini tetikliyor ve islec-govdesi iki kez koşuyor. Bu, + işaretinin derleyiciye yeni bir davranış eklemediğini, yalnız zaten var olan bir çağrıyı kısaltarak yazmanın bir yolu olduğunu doğruluyor. Add::add(a, b) yazımı, trait’in adını ve yöntemini açıkça taşıyor; a + b aynı çağrıyı taşıyor ama adı gizliyor. İkisi arasında seçim yapmak koşum sayısını hiç etkilemiyor, yalnız kaynağın okunabilirliğini etkiliyor.

Add tek başına değil; std::ops modülü her ikili işleç için benzer bir trait taşıyor — çıkarma için Sub, çarpma için Mul, dizin erişimi için Index. Hepsi aynı ilkeye dayanıyor: bir işaret, bir trait çağrısına açılıyor. Bu ders yalnız Addi ölçtü, ama sayım yöntemi hepsi için aynı kalır: her kullanım bir çağrıdır ve zincirlendiklerinde çağrı sayısı terim sayısından bir eksik olur.

Bu genelleme bir varsayım değil, Add ile aynı kalıbın diğer trait’lerde de tekrarlanmasından geliyor: Sub sub adlı, Mul mul adlı, Index index adlı tek bir yöntem bildiriyor ve her biri kendi işaretinin (-, *, []) arkasına aynı biçimde gizleniyor. Bir tipin birden fazla işleci aşırı yüklemesi de mümkün — Uzunluk isteseydi hem Add hem Sub gerçekleştirebilirdi, ikisi de bağımsız impl bloklarında dururdu ve biri diğerinin koşum sayısını etkilemezdi. Bu ders yalnız Adde odaklandı çünkü kursun ölçüm çekirdeği zaten onu taşıyordu; ilke kalan işleçler için de aynı kalıyor.

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

İşleç gövdesi sıradan bir yöntemdir, ama çağrıldığı yer kaynakta görünmez. a.clone() + b yazımında add sözcüğü hiçbir yerde geçmiyor — çağrı, + işaretinin arkasında gizli. Bir okuyucu kaynağa bakarak “bu ifadede kaç çağrı var” sorusunu, işaretleri sayarak yanıtlar: her + bir çağrıdır. Ama zincir uzadıkça, özellikle bir döngü ya da bir yineleyici zinciri içinde üretildiğinde, kaynaktaki + işaretlerini saymak da yeterli olmayabilir — döngünün kaç kez çalıştığı da bilinmesi gereken bir bilgidir. Yukarıdaki beş terimli döngüde kaynakta tek bir + yazıyor (uzun_toplam = uzun_toplam + terim;), ama bu satır döngü içinde dört kez çalıştığı için toplam çağrı sayısı dört. Kaynaktaki işaret sayısı ile çalışma zamanındaki çağrı sayısı burada ayrışıyor: kaç kez koştuğu ancak sayılarak, ancak koşum defteriyle bilinebiliyor — kaynağı okuyarak değil.

Bu, kursun baştan beri sorduğu sorunun son biçimi: kaynakta yazdığın bir gövde kaç kez koştu? İşleç aşırı yüklemede bu soru en görünmez haliyle karşımıza çıkıyor, çünkü çağrının kendisi bir ad taşımıyor — yalnız bir işaret. Kurs boyunca ölçülen on altı gövdenin sonuncusu da bu işaretin arkasında duruyordu.

Bu görünmezlik, kursun ilk dersindeki bir gözlemi tersinden doğruluyor. Rust’ın Tasarım Hedefleri dersi, HashSet::insert ile Vec::pushin aynı işi (ekleme) yaptığını ama imzalarına bakarak farklı sınıflara girdiğini göstermişti — sınıf, kaynaktan okunarak belirlenebiliyordu. İşleç aşırı yüklemede ise sınıf (her seferinde) kaynaktan yine okunabiliyor — + her zaman bir çağrıdır — ama çağrı sayısı kaynaktan okunamıyor, çünkü kaynaktaki her + işareti bir döngü içindeyse birden fazla kez çalışabiliyor. Kurs boyunca “sınıf kaynaktan okunur, sayı koşumdan gelir” ayrımı burada en net haliyle görünüyor.

Bu altı ders, aynı soruyu altı farklı düzenekte sordu. Trait Tanımı ve Gerçekleştirimi, kaynakta yazılı bir varsayılan gövdenin bir tip için koşup başka bir tip için hiç koşmadığını gösterdi. Trait Sınırları, o gövdenin etrafına konan bir koşulun koşum sayısına hiç dokunmadığını gösterdi. İlişkili Tipler, aynı sözleşmenin iki farklı yazımının koşum sayısını eşit bıraktığını, farkın çağıranın yazdığında olduğunu gösterdi. Nesne Güvenliği ve Dinamik Gönderim, çağrının derleme zamanında mı koşum zamanında mı çözüldüğünün de sayıyı değiştirmediğini gösterdi. Türetilen Trait’ler, bir gövdenin kaynakta hiç yazılı olmadan da koştuğunu, ama bu koşumun sayılamadığını gösterdi. Ve bu ders, yazılı bir gövdenin çağrısının kaynakta görünmez olabileceğini gösterdi. Altısı birlikte, “kaç kez koştu” sorusunun kaynaktaki görünürlükten bağımsız bir soru olduğunu ortaya koyuyor: bazen görünen gövde koşmuyor, bazen görünmeyen bir çağrı koşuyor.

Özet

  • Bir işlecin (+ gibi) kullanımı, standart kitaplıktaki bir trait’in (Add) yöntem çağrısına karşılık gelir; kaynakta görünen kısayol arkasında sıradan bir çağrı taşır.
  • İşleç gövdesi (islec-govdesi) her seferinde sınıfına girer: üç terimli bir toplamda iki kez, beş terimli bir toplamda dört kez koşar — çağrı sayısı her zaman terim sayısından bir eksiktir.
  • İşlecin sözdizimsel kısalığı çağrı sayısını değiştirmez; Uzunluk::addi elle çağırmakla + yazmak aynı sayıda çalışır.
  • Çağrı yeri kaynakta görünmediği için (yalnız bir işaret görünür), kaç kez koştuğu kaynağı okuyarak değil, ancak koşum defteriyle sayılarak bilinebilir.

Bu genel kural — n terim, n eksi bir çağrı — yalnız Add için değil, ikili işleçleri zincirleyen her yazım için geçerli. Zincirin uzunluğu kaynakta bir kez yazılıp bir döngüyle tekrarlanıyorsa (yukarıdaki beş terimli örnekte olduğu gibi), çağrı sayısını önceden bilmenin tek yolu döngünün kaç kez çalışacağını bilmek; kaynaktaki işleç işaretlerini saymak tek başına yeterli değil. Bu, kursun başından beri sürdürdüğü ayrımın son örneği: kaynak neyin koşacağını söylüyor, koşum kaç kez koştuğunu söylüyor ve ikisi aynı bilgiyi taşımıyor.

Sonraki Adım

Bu kurs, bir gövdenin ne zaman koştuğunu — her seferinde, yalnız gerektiğinde, hiç — sorarak trait’leri, sınırları, ilişkili tipleri, gönderim biçimlerini, türetilmiş gövdeleri ve işleç çağrılarını ölçtü. Sıradaki konu aynı soruyu bir gövdenin başarısız olabileceği yerlere taşıyor: bir değerin yokluğu tipte nasıl ifade edilir ve o yokluğu karşılayan bir gövde her zaman mı, yoksa yalnız gerektiğinde mi koşar?

İ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