Ders 05 / 14
Ödünç Denetleyicisi
Ödünç denetleyicisi, derleme zamanında bir değerin sahiplik ve ödünç kurallarını sınar; üç redd örneği (taşıma sırasında canlı bir ödünç, okurken değiştirme, yerel bir sahibe referans döndürme) hep düzyazıda tarif edilir, yanlarına kabul edilen çözüm koşturulur. Aynı işi yapan iki farklı çözüm aynı defter kaydını üretebilir; ödünç kurallarına uyan bir kod ise yine de yanlış bir sonuç verebilir — denetleyici doğruluğu değil, yalnız sahiplik disiplinini güvence altına alır.
Yayın Güncelleme
Bu derste
- Ödünç denetleyicisinin derleme zamanında sınadığı şeyi üç redd örneğiyle (taşıma sırasında ödünç, okurken değiştirme, yerel referans döndürme) gösterir
- Aynı işi yapan iki farklı çözümün (kapsam daraltma, kopya alma) aynı defter kaydını ürettiğini koşturarak kanıtlar
- Ödünç kurallarına tam uyan bir kodun yine de yanlış bir sonuç verebileceğini örnekle kanıtlar
İçindekiler
Önceki dört ders ödünç almanın kurallarını tek tek kurdu: sahiplik vermeden erişim, kaynağın bitmemesi, ömrün kaynağı aşmaması, paylaşımlı/değiştirilebilir ayrımı. Bu ders bu kuralları uygulayan mekanizmanın kendisine bakıyor: ödünç denetleyicisi derleme zamanında tam olarak neyi sınıyor? Denetleyicinin izlediği şey bir değerin ömrüdür — Eşzamanlılık kursunda (M09/K03) sorulan “koşan bir birim nasıl duracak” sorusu değil.
Dört dersin de ortak bir varsayımı vardı: gösterilen her kod zaten kurala uyuyordu, kabul edilen tarafta duruyordu. Bu ders o çizginin kendisini görünür kılıyor — kuralın neyi ayırdığını, hem reddedilen tarafı düzyazıyla tarif ederek hem kabul edilen tarafı koşturarak gösteriyor. Aynı işi yapan birden çok kabul edilen yazımın var olabilmesi de bu dersin ayrı bir bulgusu: kural tek bir doğru biçim dayatmıyor, yalnız bir sınır çiziyor.
Derleyicinin bir yazımı reddetmesi bu müfredatta ilk kez Go’nun Tasarım Amaçları dersinde (M09/K01) ana ölçü ekseniydi — orada kaynağın derlenip derlenmediğinin kendisi ölçülüyordu. Bu kursta aynı soru kursun ana ölçümü olamaz; yalnız bu dersin sınırlı payı. Kalan dört dersin hiçbirinde olmadığı kadar çok redd bu derste görülüyor, ama her biri aynı kurala bağlı kalıyor: redd düzyazıda tarif edilir, kabul edilen çözüm koşturulur.
Önceki üç dersteki reddler tek tek, birer kuralın somut örneğiydi — bu derste üç redd yan yana durup denetleyicinin genel çalışma biçimini gösteriyor. Üçünün de ortak yanı şu: denetleyici bir kaynağı satır satır okumuyor, kaynaktaki her değerin ne kadar süre geçerli olduğunu ve o süre boyunca kimin hangi yetkiyle eriştiğini bir bütün olarak izliyor. Bir satırın kendi başına geçerli görünmesi yetmiyor; o satırın önceki ve sonraki satırlarla birlikte kurduğu ilişki de kurala uymak zorunda.
Redd 1: Taşıma Sırasında Canlı Bir Ödünç
Bir değer ödünç verilmişken taşınamaz. degere bir referans (bakis) alınmışken aynı
degeri bir işleve taşımaya çalışan bir çağrı derlenmez — çünkü taşıma eski adı geçersiz
kılıyor, ama bakis hâlâ o adın işaret ettiği yere bakıyor olurdu. Bu ihlali gösteren bir
kaynak hiç yazılmıyor; onun yerine aynı işi yapan iki ayrı kabul edilen çözüm
koşturuluyor ve ikisinin de aynı defter kaydını ürettiği ölçülüyor.
Bu redd, Sahiplik ve Taşıma dersindeki tek-sahip kuralı ile Ödünç Alma dersindeki kapsam-içi-kalma kuralının kesiştiği yer. Taşıma tek başına serbest, ödünç alma tek başına serbest; ikisi aynı değer üzerinde, aynı anda çakıştığında denetleyici devreye giriyor. Taşıma eski adı geçersiz kılarken elde kalan bir referansın ne olacağı belirsizleşiyor — denetleyici bu belirsizliği, taşımaya izin vermeden önce üzerindeki bütün ödünçlerin kapanmış olmasını isteyerek gideriyor.
Çözüm A — kapsam daraltma: referans kendi dar kapsamında bırakılıp taşımadan önce kapatılıyor.
// iz.rs — bir degerin bitisini kaydeden olcum yardimcisi
use std::cell::RefCell;
use std::rc::Rc;
pub type Defter = Rc<RefCell<Vec<String>>>;
pub struct Iz {
pub ad: String,
defter: Defter,
}
impl Iz {
pub fn yeni(ad: &str, defter: &Defter) -> Iz {
Iz { ad: ad.to_string(), defter: Rc::clone(defter) }
}
}
impl Drop for Iz {
fn drop(&mut self) {
self.defter.borrow_mut().push(self.ad.clone());
}
}
pub fn yeni_defter() -> Defter {
Rc::new(RefCell::new(Vec::new()))
}
pub fn okunan(defter: &Defter) -> Vec<String> {
defter.borrow().clone()
}
// kapsam_daraltma.rs — odunc, tasimadan once dar bir kapsamda bitiriliyor
mod iz;
use iz::{Iz, yeni_defter, okunan};
fn gorevi_isle(deger: Iz) {
println!("islev icinde tasinan ad: {}", deger.ad);
}
fn main() {
let defter = yeni_defter();
let deger = Iz::yeni("gorev", &defter);
{
let bakis = °er;
println!("mevcut ad: {}", bakis.ad);
}
gorevi_isle(deger);
println!("kapsam daraltma sonrasi defter: [{}]", okunan(&defter).join(", "));
}
mevcut ad: gorev islev icinde tasinan ad: gorev kapsam daraltma sonrasi defter: [gorev]
Çözüm B — kopya alma: okuma için gereken veri baştan kopyalanıyor, degere hiç
referans alınmıyor.
// kopya_alma.rs — okunacak veri onceden kopyalanip odunc hic alinmiyor
mod iz;
use iz::{Iz, yeni_defter, okunan};
fn gorevi_isle(deger: Iz) {
println!("islev icinde tasinan ad: {}", deger.ad);
}
fn main() {
let defter = yeni_defter();
let deger = Iz::yeni("gorev", &defter);
let mevcut_ad = deger.ad.clone();
println!("mevcut ad: {}", mevcut_ad);
gorevi_isle(deger);
println!("kopya alma sonrasi defter: [{}]", okunan(&defter).join(", "));
}
mevcut ad: gorev islev icinde tasinan ad: gorev kopya alma sonrasi defter: [gorev]
SK13. İki çözüm de aynı defter kaydını ([gorev]) üretiyor. Denetleyici tek bir doğru
yazım dayatmıyor; yalnız bir kuralı (taşıma sırasında canlı bir ödünç olamaz) uyguluyor ve
bu kuralı sağlayan her yazım kabul ediliyor. Kapsam daraltma referansın ömrünü kısaltıyor,
kopya alma referansı hiç kurmuyor — ikisi de farklı bir yoldan aynı kurala uyuyor ve aynı
gözlemlenebilir sonuca varıyor.
İki çözüm arasındaki fark maliyette. Kapsam daraltma hiçbir yeni veri kurmuyor, yalnız
referansın kapsamını daraltıyor; kopya alma ise deger.adin bir kopyasını çıkarıyor —
Izin kendisi hâlâ tek, ama ad alanının içeriği bir kez daha kuruluyor. İki yazım da
aynı kurala uyuyor, ama hangisinin seçileceği kaynağın geri kalanına bağlı: referansın
kapsamı dar bir bloğa sığdırılabiliyorsa kapsam daraltma tercih edilir; okunacak veri
işlevin farklı noktalarında tekrar tekrar gerekiyorsa kopya almak daha sade kalabilir.
Redd 2: Okurken Değiştirme
Bir listenin üzerinde paylaşımlı bir ödünçle (örneğin bir döngüyle) gezinirken aynı listeyi değiştirmeye çalışmak da reddediliyor — döngünün kendisi listeye bir paylaşımlı referans tutarken, listeye eleman eklemek değiştirilebilir bir referans istiyor ve ikisi çakışıyor. Çözüm, okuma ile yazmayı iki ayrı adıma bölmek. Bu kalıp, bir koleksiyonun her elemanından türeyen yeni elemanları aynı koleksiyona eklemek istendiğinde sıkça karşılaşılan bir durum — koleksiyonun kendi üzerinde gezinirken kendisini büyütmeye çalışmak.
// okuma_sonra_yazma.rs — once paylasimli odunc ile okunur, sonra ayri bir adimda degistirilir
mod iz;
use iz::{Iz, yeni_defter, okunan};
fn main() {
let defter = yeni_defter();
let mut liste = vec![Iz::yeni("p", &defter), Iz::yeni("q", &defter)];
let mut kopya_adlari = Vec::new();
for iz in &liste {
kopya_adlari.push(format!("{}-kopya", iz.ad));
}
for ad in &kopya_adlari {
liste.push(Iz::yeni(ad, &defter));
}
println!("liste uzunlugu: {}", liste.len());
println!("henuz biten yok, defter: [{}]", okunan(&defter).join(", "));
}
liste uzunlugu: 4 henuz biten yok, defter: []
İlk döngü &liste üzerinde gezinip her adın bir kopyasını ayrı bir Vec<String>e
topluyor; bu döngü bittiğinde paylaşımlı ödünç de kapanıyor. İkinci döngü listeyi
serbestçe değiştiriyor, çünkü artık üzerinde hiçbir ödünç yok. Aynı işi tek bir döngüde,
gezinme sürerken listeye eleman ekleyerek yapmaya çalışan bir yazım derlenmezdi.
Bu redd Redd 1’le aynı aileden, ama kaynağı farklı. Redd 1’de çakışan iki şey bir taşıma
ile bir ödünçtü; burada çakışan iki şey bir paylaşımlı ödünç (döngünün kendisi) ile bir
değiştirilebilir ödünç (push çağrısı). İkisinin ortak noktası, denetleyicinin çakışmayı
satırların sırasına değil, ödünçlerin kapsamlarının örtüşüp örtüşmediğine bakarak
bulması. Döngü listenin her elemanını teker teker ziyaret ederken bile, döngünün kendisi
liste üzerinde tek, sürekli bir paylaşımlı ödünç tutuyor — döngü bitene kadar kapanmıyor.
Redd 3: Yerel Bir Sahibe Referans Döndürmek
Bir işlev kendi içinde kurduğu bir değere referans döndüremez — işlev bitince o değerin sahibi (işlevin kendi kapsamı) da bitiyor, döndürülen referans artık var olmayan bir yere işaret ederdi. Çözüm, Sahiplik ve Taşıma dersinde kurulan düzenin aynısı: referans değil, değerin kendisini döndürmek.
İlk iki redd, ödünç ile taşımanın ya da ödünç ile başka bir ödüncün aynı kapsamda çakışmasıydı — her ikisinde de sorunlu iki erişim aynı satırlar arasında, aynı kaynak içinde duruyordu. Üçüncü redd farklı: sorun tek bir işlevin sınırını aşıyor, çağıran tarafla çağrılan taraf arasında.
// sahiplik_dondur.rs — islev yerel degeri referans degil, sahiplikle donduruyor
mod iz;
use iz::{Iz, yeni_defter, okunan, Defter};
fn olustur(ad: &str, defter: &Defter) -> Iz {
let yerel = Iz::yeni(ad, defter);
yerel
}
fn main() {
let defter = yeni_defter();
let deger = olustur("uretilen", &defter);
println!("uretilen ad: {}", deger.ad);
println!("deger hala sahibinde, defter: [{}]", okunan(&defter).join(", "));
}
uretilen ad: uretilen deger hala sahibinde, defter: []
olustur işlevi yereli kendi gövdesinde kuruyor ve onu taşıyarak döndürüyor —
&yerel değil, yerelin kendisi. Sahiplik çağırana geçtiği için deger mainin sonuna
kadar geçerli kalıyor; defter hâlâ boş. olusturun imzası &Iz döndürseydi, derleyici bu
imzayı bile kabul etmezdi, çünkü döndürülecek referansın hangi kaynağa bağlı olduğu
belirsiz kalırdı — işlev içinde kurulan yerelin kendisi işlev bitince bitiyor.
Bu redd önceki ikisinden farklı bir yerde duruyor: Redd 1 ve Redd 2 belirli bir çağrı
sırasını yasaklıyordu, kaynağın kendisi baştan itibaren geçersizdi. Burada asıl reddedilen
şey, işlevin imzası — fn olustur(...) -> &Iz yazımı, gövdesine hiç bakılmadan
sorunlu. Bir işlevin döndürdüğü bir referansın nereye bağlı olduğu, işlevin çağrıldığı her
yerde bilinmesi gereken bir bilgi; işlev kendi içinde kurduğu bir değere referans
döndürdüğünde bu bilgi kaynakta hiçbir yerde yazılı değil. Sahiplik ve Taşıma dersinde
görülen “işlevin sahip olduğu bir değeri geri döndürmesi bir taşımadır” kuralı burada tek
çözüm olarak geri dönüyor.
SK14. Bu redd, bir referansın geçerli olduğu aralığın kaynağının kapsamını hiçbir zaman aşamaması kuralının (Ödünç Alma dersi) doğrudan bir sonucu. Burada henüz adı konmamış bir kavram devreye giriyor: bir referansın “ne kadar süre” geçerli kalabileceğini belirleyen bu aralığın kendi bir adı var, ama bu ders o adı vermiyor.
Bu redd, ilk iki reddin aksine, işlevin çağrıldığı hiçbir yerdeki koda bakmadan, yalnız
olusturun kendi gövdesi okunarak tespit edilebiliyor. Denetleyici bunu, işlevin imzasına
bakıp döndürülen referansın hangi parametreye ya da hangi yerel değere bağlı olduğunu
izleyerek yapıyor; yerelin kaynağı işlevin dışından gelmediği için, döndürülecek bir
referansın dışarıda geçerli kalabileceği hiçbir yol yok.
Sınırlayıcı Ölçüm: Denetleyici Doğruluğu Güvence Altına Almaz
Buraya kadarki bütün örnekler denetleyicinin neyi reddettiğini gösterdi. Şimdi tersi: denetleyicinin kabul ettiği, hiçbir kuralı ihlal etmeyen bir kod, yine de yanlış bir sonuç verebilir.
// dogruluk_garantisi_yok.rs — odunc kurallarina uyan kod yine de yanlis sonuc verebilir
fn ekle_ve_say(liste: &mut Vec<i32>, deger: i32) -> usize {
let uzunluk = liste.len();
liste.push(deger);
uzunluk
}
fn main() {
let mut liste = Vec::new();
let sayim1 = ekle_ve_say(&mut liste, 10);
let sayim2 = ekle_ve_say(&mut liste, 20);
println!("islevin dondurdugu sayimlar: {}, {}", sayim1, sayim2);
println!("listenin gercek uzunlugu: {}", liste.len());
}
islevin dondurdugu sayimlar: 0, 1 listenin gercek uzunlugu: 2
ekle_ve_say hiçbir sahiplik ya da ödünç kuralını ihlal etmiyor — listeyi bir
değiştirilebilir referansla alıyor, elemanı ekliyor, geçerli bir sayı döndürüyor. Ama
uzunluk eklemeden önceki uzunluğu tutuyor, döndürülen değer ise “eklemeden sonraki
sayım” olarak okunmak isteniyorsa yanlış: iki çağrı 0 ve 1 döndürüyor, oysa listenin
gerçek uzunluğu sırasıyla 1 ve 2 olmalıydı.
SK15. Ödünç denetleyicisi yalnız sahiplik ve erişim disiplinini sınıyor — bir değere aynı anda kimin, ne yetkiyle eriştiğini. Bir hesabın hangi noktada okunduğu, bir değişkenin doğru satırda güncellenip güncellenmediği bu denetimin dışında; kaynak tamamen geçerli bir sahiplik/ödünç düzeni taşırken de mantık hatası barındırabilir. Bu dersin ölçtüğü tek şey şu: bir kodun derlenmesi, onun doğru olduğu anlamına gelmiyor.
Hatanın düzeltilmesi de aynı gözlemi tamamlıyor: sorun sahiplikte değil, uzunlukun hangi
satırda okunduğunda.
// duzeltilmis.rs — ayni islev, uzunluk eklemeden sonra okunuyor
fn ekle_ve_say(liste: &mut Vec<i32>, deger: i32) -> usize {
liste.push(deger);
liste.len()
}
fn main() {
let mut liste = Vec::new();
let sayim1 = ekle_ve_say(&mut liste, 10);
let sayim2 = ekle_ve_say(&mut liste, 20);
println!("islevin dondurdugu sayimlar: {}, {}", sayim1, sayim2);
println!("listenin gercek uzunlugu: {}", liste.len());
}
islevin dondurdugu sayimlar: 1, 2 listenin gercek uzunlugu: 2
liste.push(deger) ile liste.len()in yeri değiştirildi, başka hiçbir şey değişmedi —
parametre tipi, ödünç, imza aynı kaldı. Düzeltilmiş sürüm de tıpkı hatalı sürüm gibi
ödünç denetleyicisinden geçiyor, çünkü denetleyici ikisi arasındaki farkı zaten hiç
görmüyor: her iki sürümde de listeye bir kez değiştirilebilir referans alınıyor, bir kez
kullanılıyor, kural ihlal edilmiyor. Denetleyicinin sessiz kalması, kodun doğru olduğunun
kanıtı değil, yalnız erişim kurallarının ihlal edilmediğinin kanıtı.
Üç redd ve bir doğruluk sınırı, birlikte ödünç denetleyicisinin sınırlarını çiziyor. Denetleyici bir kaynağı reddettiğinde, sebep her zaman aynı ailede: bir değere aynı anda birden fazla, birbiriyle çelişen erişimin var olması (Redd 1, Redd 2) ya da bir referansın kaynağından daha uzun yaşayacak olması (Redd 3). Kabul ettiğinde ise denetleyicinin söylediği şey dardır — yalnız bu erişim düzeninin güvenli olduğu, kodun yaptığı hesabın doğru olduğu değil. Dört ders boyunca kurulan sahiplik, taşıma, ödünç ve dilim kuralları bu dersle birlikte tek bir mekanizmaya, tek bir denetime bağlanıyor; ama o mekanizmanın sınadığı şeyin sınırları da aynı netlikte görülüyor.
Özet
- Ödünç denetleyicisi, derleme zamanında sahiplik ve ödünç kurallarının ihlal edilip edilmediğini sınıyor; ihlal eden kaynak hiç derlenmiyor.
- Taşıma sırasında canlı bir ödünç olamaz; kapsam daraltma ve kopya alma bu kısıtı iki farklı yoldan sağlayabiliyor ve ikisi de aynı defter kaydını üretiyor: değer iki yazımda da kapsam sonunda bitiyor.
- Bir kaynak üzerinde paylaşımlı bir ödünçle gezinirken aynı kaynağı değiştirmek reddediliyor; okuma ve yazma ayrı adımlara bölünerek çözülüyor.
- Bir işlev kendi içinde kurduğu bir değere referans döndüremez; çözüm, referans değil değerin kendisini (sahiplikle) döndürmek.
- Ödünç kurallarına tam uyan bir kod yine de yanlış bir sonuç verebilir — denetleyici doğruluğu değil, yalnız erişim disiplinini güvence altına alıyor.
- Aynı kurala uyan birden çok yazım (kapsam daraltma, kopya alma) kabul edilebilir; denetleyici tek bir doğru biçim dayatmıyor, yalnız bir sınır çiziyor.
Sonraki Adım
Bu derste denetleyicinin sınadığı şeyin bir aralık olduğu defalarca görüldü — bir referansın ne kadar süre geçerli kalabileceğini belirleyen bir sınır. Redd 1’de bu aralık bir taşımayla çakışmamak zorundaydı, Redd 2’de bir değişiklikle çakışmamak zorundaydı, Redd 3’te ise kaynağının kendi kapsamını hiçbir zaman aşamıyordu. Üçü de aynı şeyden söz ediyor, ama bu aralığın kendi adı bu beş ders boyunca hiç konmadı. Sıradaki bölüm tam olarak bu adı koyuyor: bir referansın geçerlilik aralığının kendi kavramı, bu aralığın ne zaman kaynaktan okunabildiği ve ne zaman açıkça yazılması gerektiği.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.