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.