Ders 12 / 15
Kapanışlar
Bir kapanışın gövdesi tanımlandığı satırda değil çağrıldığı anda koşuyor: aynı iş doğrudan çağrı, gecikmeli çağrı ve hiç çağrılmayan kapanış biçiminde yazılıp koşum sayıları karşılaştırılıyor ve taşıma ile yakalamanın kaynaktaki değere ne yaptığı ölçülüyor.
Yayın Güncelleme
Bu derste
- Aynı işin doğrudan çağrı, gecikmeli çağrı ve hiç çağrılmayan kapanış biçimlerinde kaç kez koştuğunu koşturarak karşılaştırır
- Taşıma ile yakalanan bir değerin ömrünün kapanışın kendisine bağlandığını, gövdenin koşup koşmamasından bağımsız olarak gösterir
- Kapanışın tanımlanmasının koşum defterine girmeyen ayrı bir iş olduğunu ayırt eder
İçindekiler
Önceki konu, bir gövdenin kalanının hiç çalışmayabileceğini ölçtü: erken dönüş ya da kurtarılamaz bir hata, kaynakta yazılı adımları sessizce atlıyordu. O ölçüm, hangi dalın koştuğuna bakıyordu — gövdenin kendisi her zaman aynı yerde, aynı çağrının içindeydi. Bu ders aynı “kaç kez koştu” sorusunu daha küçük bir birime indiriyor ve dalı değil saklama biçimini değiştiriyor: artık bir gövdenin bir kez bile koşup koşmayacağı, hangi denetim dalına düştüğüne değil, o gövdenin çağrılmadan önce nasıl saklandığına bağlı. Bir kapanış, çevresinden bir değer yakalayıp bir gövdeyi sonraya bırakabilen bir değerdir; bu tanım Programlama Temelleri kursunda kuruldu ve burada yeniden yazılmıyor. Bu derste ölçülen, aynı işin üç farklı saklama biçiminde kaç kez koştuğu, aynı kapanışın birden çok kez çağrılmasının gövdeyi kaç kez koşturduğu ve yakalamanın kaynaktaki değere ne yaptığıdır. Üç ölçüm de aynı koşum defterini kullanıyor: bir gövde çalıştığında kendi adını bu deftere işliyor, çalışmadığında hiçbir iz bırakmıyor. Sayaç bu yüzden bir tahmin değil, koşumun kendisinden okunan bir kayıttır.
Aynı İş, Üç Çağrılma Biçimi
Bir gövde üç biçimde yazılabilir: sıradan bir işlev olarak tanımlanıp hemen çağrılabilir; bir kapanışa sarılıp bir adın altında saklanıp sonra çağrılabilir; ya da bir kapanışa sarılıp hiç çağrılmayabilir. Kaynak metni üçünde de neredeyse aynıdır — fark, tanımlandığı yerle çağrıldığı yer arasındaki mesafede yatar. Sıradan bir işlev çağrısında bu mesafe sıfırdır: tanım ile çağrı aynı satırdadır. Bir kapanış saklandığında bu mesafe açılır ve gövde, adı tutulduğu sürece, istenildiği an — ya da hiç — çağrılabilir hale gelir. Bu mesafenin kendisi bir ölçüm konusu değildir; ölçülen, mesafe ne kadar açılırsa açılsın gövdenin sonunda çağrılıp çağrılmadığıdır.
Aşağıdaki koşumda dogrudan_govde bir kapanış değil, iç içe tanımlanmış sıradan bir
işlevdir; bu ayrım bilinçli tutuldu, çünkü Rust’ta iç içe tanımlanan bir işlev çevresinden
hiçbir şey yakalayamaz — defter ona parametre olarak açıkça geçiriliyor. Kapanış
sözdizimi (||) ise bir gövdeyi, çevresindeki adlara bağlı bir değere çevirir; sıradan bir
işlevle karşılaştırmanın adil kalması için burada bu fark not düşülüyor. Kıyaslanan şey
“kapanış mı değil mi” değil, gövdenin tanımlandığı yerle çağrıldığı yer arasındaki mesafe.
YN1. — “aynı işin doğrudan çağrıda koştuğu, gecikmeli kapanışta çağrılana kadar koşmadığı ve hiç çağrılmayan kapanışta koşumun sonuna kadar hiç koşmadığı” iddiası, aşağıdaki koşumla doğrulanıyor.
// kapanis_govdesi.rs — ayni is uc bicimde: dogrudan cagri, gecikmeli cagri, hic cagrilmayan kapanis
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Defter = Rc<RefCell<BTreeMap<String, u32>>>;
fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
// dogrudan govde: bir kapanis degil, tanimlanip hemen cagrilan sirasiyla bir islev
fn dogrudan_govde(defter: &Defter) {
say(defter, "dogrudan-govde");
}
dogrudan_govde(&defter);
// gecikmeli govde: bir kapanista saklaniyor, tanimlandigi satirda henuz kosmuyor
let gecikmeli = || say(&defter, "gecikmeli-govde");
println!("tanimlandiktan hemen sonra: {}", kere(&defter, "gecikmeli-govde"));
gecikmeli();
println!("cagirdiktan sonra: {}", kere(&defter, "gecikmeli-govde"));
// cagrilmayan kapanis: kursun olcum cekirdegindeki ayni kesit, hic cagrilmiyor
let _kapanis = || say(&defter, "cagrilmayan-kapanis");
println!("dogrudan govde: {}", kere(&defter, "dogrudan-govde"));
println!("cagrilmayan kapanis: {}", kere(&defter, "cagrilmayan-kapanis"));
}
tanimlandiktan hemen sonra: 0 cagirdiktan sonra: 1 dogrudan govde: 1 cagrilmayan kapanis: 0
Gecikmeli gövde tanımlandığı satırda henüz koşmuyor: kapanış tanımlandıktan hemen sonra
okunan sayaç sıfır. Açıkça çağrıldığında sayaç bire çıkıyor — tanımlanma ile çağrılma
arasındaki satırlar, gövdenin koşum sayısını hiç etkilemiyor; araya kaç satır, kaç işlev
çağrısı ya da kaç dallanma girerse girsin, gecikmeli çağrılana kadar sayaç sıfırda
kalmaya devam ederdi. cagrilmayan-kapanis adlı üçüncü gövde ise kursun ölçüm
çekirdeğinin aynı satırıdır: _kapanis adı altında saklanıyor ve programın geri kalanında
hiç çağrılmıyor. Bu satır, kursun bağlayıcı tablosunda hiç sınıfına düşüyor —
kaynakta yazılı olmak koşmuş olmak anlamına gelmiyor. Bu ölçü, M08/K04’ün ölçtüğü “adımı
hangi iş parçacığı koşturuyor” sorusuyla karışmaz: burada tek bir iş parçacığı var, soru
yalnızca gövdenin kaç kez koştuğudur.
Kapanış Kaç Kez Çağrılırsa Gövde O Kadar Koşar
Bir önceki ölçüm, aynı gövdenin bir kez çağrılıp çağrılmadığına bakıyordu. Ama bir kapanış tek çağrıyla sınırlı değildir: aynı ad altında saklanan aynı kapanış, art arda birden çok kez çağrılabilir ve her çağrı gövdeyi yeniden koşturur — bir öncekinin sonucu hatırlanmaz, saklanmaz, yeniden kullanılmaz. Kapanışı sıradan bir değerden ayıran şey gövdesinin içeriği değil, gövdenin bir ada bağlanıp istenildiği kadar çağrılabilmesidir. Bu, “hiç” ile “en az bir kez” arasındaki farktan ayrı bir soru: bir kapanış hiç çağrılmayabileceği gibi, aynı kapanış birden çok kez de çağrılabilir ve bu ikinci durumda sayaç çağrı sayısını birebir izler.
YN2. — “aynı kapanışın art arda üç kez çağrılmasının gövdeyi bir değil üç kez koşturduğu” iddiası, aşağıdaki koşumda üç ayrı çağrının sayacı üçe çıkarmasıyla doğrulanıyor.
// coklu_cagri.rs — govdesi yakaladigi degeri sadece okuyan bir kapanis defalarca cagrilabiliyor
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Defter = Rc<RefCell<BTreeMap<String, u32>>>;
fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
let etiket = String::from("olcum-etiketi");
let coklu_cagrilan = || {
say(&defter, "coklu-cagrilan-govde");
etiket.len()
};
let a = coklu_cagrilan();
let b = coklu_cagrilan();
let c = coklu_cagrilan();
println!("uc cagridan sonra uzunluklar: {a}, {b}, {c}");
println!("govde kac kez kosti: {}", kere(&defter, "coklu-cagrilan-govde"));
}
uc cagridan sonra uzunluklar: 13, 13, 13 govde kac kez kosti: 3
coklu_cagrilan, yakaladığı etiketi yalnızca okuyor — uzunluğunu döndürüyor, değerin
kendisini gövdesinden dışarı taşımıyor. Bu yüzden aynı kapanış üç kez çağrılabiliyor ve her
çağrı sayacı bir artırıyor: üç çağrı, üç koşum. Eğer bu gövde etiketi okumak yerine
değerin sahipliğini dışarı taşısaydı — örneğin etiketin kendisini olduğu gibi
döndürseydi — ilk çağrıda değeri elinden kaçırır ve derleyici ikinci çağrıyı reddederdi:
gövde artık elinde tutmadığı bir değeri bir daha veremez. Bu redd kaynak koduna konmuyor,
çünkü derlenmeyen bir kaynak dersin kapsamı dışında kalıyor; ama sonucu şudur — yakaladığı
değeri yalnızca okuyan bir kapanış istenildiği kadar çağrılabilir, yakaladığı değeri
tüketen bir kapanış yalnızca bir kez çağrılabilir. İkisi de aynı taşıma kuralının, çağrı
sayısına yansıyan farklı bir yüzüdür; bir kapanışın kaç kez çağrılabileceği, gövdesinin
yakaladığı değere ne yaptığından okunabilir. Bu üç çağrılabilirlik derecesi — istenildiği
kadar, yalnızca değiştirerek, yalnızca bir kez — Rust’ın standart kitaplığındaki üç kapanış
trait’inin karşılığıdır; bu trait’lerin adları ve aralarındaki sınır bu dersin kapsamı
dışında kalıyor, çünkü bir sözleşmenin sınırını okumak kursun ayrı bir dersinin konusu.
Burada ölçülen yalnızca çağrı sayısının kaynağa yansıyan sonucudur.
Yakalama Kaynağı Nasıl Değiştiriyor
Kapanışların koşup koşmadığı sorunun bir yarısı; ne yakaladıkları öbür yarısıdır. Bir
kapanış çevresinden bir değeri iki yoldan alabilir: ödünç alarak (referansla) ya da
taşıyarak. Bu seçim gövdenin kaynak metninde açık değildir — derleyici, gövdenin yakalanan
her ada ne yaptığına bakarak kararı kendisi verir; move anahtar sözcüğü bu kararı elle
zorlar. Taşıma, Sahiplik ve Bellek Modeli kursunda kurulan sahiplik kuralının kapanışlara
uygulanmış biçimidir: bir değer bir kapanışa taşındığında, o değerin sahibi artık kapanışın
kendisidir ve değerin ömrü kapanışın ömrüne bağlanır — gövdesi hiç koşmasa bile.
YN3. — “taşıma ile yakalanan bir değerin, kapanışın gövdesi hiç koşmasa bile kapanışla
birlikte bırakıldığı” iddiası, aşağıdaki koşumda yakalanan-deger-birakildi sayacının
kapanış bırakılana kadar sıfır kalıp bırakılınca bire çıkmasıyla, buna karşılık gövdenin
kendi sayacının koşum boyunca sıfırda kalmasıyla doğrulanıyor.
// yakalama_omru.rs — tasima ile yakalanan degerin omru kapanisin kendisine baglaniyor
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Defter = Rc<RefCell<BTreeMap<String, u32>>>;
fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
struct Kayit {
defter: Defter,
}
impl Drop for Kayit {
fn drop(&mut self) {
say(&self.defter, "yakalanan-deger-birakildi");
}
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
let kayit = Kayit { defter: Rc::clone(&defter) };
// kapanis "kayit"i tasiyarak yakaliyor: bu satirdan sonra "kayit" adi kullanilamaz
let hic_cagrilmayan = move || {
say(&kayit.defter, "cagrilmayan-kapanis-2");
};
println!(
"kapanis tanimlandiktan sonra, hic cagrilmadan: {}",
kere(&defter, "yakalanan-deger-birakildi")
);
drop(hic_cagrilmayan);
println!("kapanis birakildiktan sonra: {}", kere(&defter, "yakalanan-deger-birakildi"));
println!("govdesi hic kosmadi: {}", kere(&defter, "cagrilmayan-kapanis-2"));
}
kapanis tanimlandiktan sonra, hic cagrilmadan: 0 kapanis birakildiktan sonra: 1 govdesi hic kosmadi: 0
Kayit değeri move anahtar sözcüğüyle kapanışa taşınıyor; bu satırdan sonra kayit adı
artık kullanılamaz — değerin sahibi kapanış oldu. Kapanış hiç çağrılmadan bırakıldığında
(bir çağrıyla açıkça bitirildiğinde), taşınan Kayit değeri de kapanışla birlikte bitiyor
ve bırakma izi deftere düşüyor. Bu, gövdenin koşum sayacından tümüyle ayrı bir olay:
cagrilmayan-kapanis-2 sayacı koşum boyunca sıfırda kalırken, yakalanan değerin bırakılma
sayacı bire çıkıyor. Kapanışın tanımlanması ve taşınan değeri elinde tutması, gövdesi hiç
çağrılmamasına rağmen kaynak üzerinde gerçek bir etki bırakıyor — yalnızca bu etki, gövde
koşum sayacının ölçtüğü şey değil. Kapanış açıkça bırakılmasaydı bile, kapsamının sonuna
gelindiğinde aynı bırakma yine gerçekleşirdi; burada açık çağrı yalnızca anı gözlemlenebilir
kılmak için kullanıldı, sonucu değiştirmedi.
Bir referansla yakalama, taşımadan farklı olarak kaynaktaki adı canlı tutar; Sahiplik ve Bellek Modeli kursunda kurulan ödünç alma kuralları burada da geçerlidir ve bu ders onları yeniden kurmuyor.
YN4. — “referansla yakalayan bir kapanışın, yakaladığı adı kapanış tanımlandıktan
sonra da ve kapanış çağrıldıktan sonra da kullanılabilir bıraktığı” iddiası, aşağıdaki
koşumda etiket adının üç ayrı satırda da okunabilmesiyle doğrulanıyor.
// referansla_yakalama.rs — referansla yakalama kaynaktaki adi canli tutuyor, tasima gibi tasimiyor
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Defter = Rc<RefCell<BTreeMap<String, u32>>>;
fn say(defter: &Defter, ad: &str) {
*defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}
fn kere(defter: &Defter, ad: &str) -> u32 {
*defter.borrow().get(ad).unwrap_or(&0)
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
let etiket = String::from("paylasilan-etiket");
let referansla_yakalayan = || {
say(&defter, "referansla-yakalanan-govde");
etiket.len()
};
println!("kapanis tanimlandiktan sonra etiket hala okunabiliyor: {etiket}");
let uzunluk = referansla_yakalayan();
println!("cagridan sonra: {}", kere(&defter, "referansla-yakalanan-govde"));
println!("etiket kapanistan sonra da erisilebilir: {etiket} (uzunluk {uzunluk})");
}
kapanis tanimlandiktan sonra etiket hala okunabiliyor: paylasilan-etiket cagridan sonra: 1 etiket kapanistan sonra da erisilebilir: paylasilan-etiket (uzunluk 17)
referansla_yakalayan, move anahtar sözcüğü olmadan tanımlanıyor ve gövdesi etiketi
yalnızca okuyor; bu yüzden yakalama bir ödünç almaya indirgeniyor, taşımaya değil. etiket
adı hem kapanış tanımlandıktan hemen sonra hem de kapanış çağrıldıktan sonra doğrudan
okunabiliyor — önceki koşumdaki kayit adının taşımadan sonra kullanılamaz hale gelmesiyle
tam bir karşıtlık. Aynı yakalama sözdizimi (|| { ... }), gövdenin yakaladığı değere ne
yaptığına göre iki farklı sahiplik sonucu üretiyor: biri kaynaktaki adı kapatıyor, öbürü
açık bırakıyor. Bu ders bu iki sonucu yeniden kurmuyor, yalnızca ikisinin de gövde koşum
sayısından bağımsız olduğunu ölçüyor. Hangi yolun seçileceği kaynak metinde move yazılıp
yazılmadığından okunur; gövdenin kaç kez koştuğu ise bu seçimden değil, kapanışın kaç kez
çağrıldığından belirlenir — iki soru birbirinden bağımsız eksenlerdir.
Sınırlayıcı Ölçüm
Koşum defteri yalnız gövde çağrılarını sayıyor; bir kapanışın tanımlanması da bir
iştir, ama bu iş deftere hiç girmiyor. Bir kapanış tanımlandığında, yakaladığı değerler
(varsa) o an kopyalanıyor ya da taşınıyor, kapanışın kendisi bir değer olarak bellekte bir
yer kaplıyor — bunların hepsi gerçek işlemler, ama hiçbiri bir sayaç çağrısı içermiyor,
çünkü onları tetikleyen kod bizim yazdığımız gövde değil, derleyicinin ürettiği kapanış
temsilidir. Bu yüzden cagrilmayan-kapanisin sayacının sıfır olması, kapanışın var
olmadığı ya da bedelsiz olduğu anlamına gelmez; yalnızca gövdesinin hiç koşmadığı anlamına
gelir. Kapanışın kurulma bedeli, bu dersin ölçtüğü sayının kapsamı dışında kalıyor.
Bu sınır, tek bir kapanış için küçük görünür ama birden çok kapanış art arda kurulduğunda büyür. Bir kapanışın kendisi de bir değerse, o değer başka bir yapıya konabilir, başka bir kapanışa parametre olarak geçirilebilir ya da bir zincirin bir halkası olabilir — ve zincirin her halkası, hiç çalışmasa bile kurulma anında bir yer kaplar. Sıradaki konuda bu gözlem doğrudan işe yarayacak: birden çok kapanış birbirine bağlanıp tek bir dolaşım sözleşmesi kurduğunda, sözleşmenin kendisi kurulur ama gövdeleri istenmeden koşmaz. Bu ders o zincirin ilk halkasını — tek bir kapanışı — ölçtü.
Bu dört koşum bir araya geldiğinde ortaya çıkan tablo şudur: bir gövdenin koşup koşmadığı saklama biçimine, kaç kez koştuğu çağrı sayısına, hangi değerin ne zaman bittiği yakalama biçimine bağlıdır — ve bu üç sorunun hiçbiri birbirini belirlemez. Bir kapanış hiç çağrılmadan da bir değeri elinde tutup bırakabilir; defalarca çağrılıp hiçbir değeri taşımayabilir; ya da yakaladığı değeri tüketip tek bir çağrıyla sınırlı kalabilir. Kaynak metne bakarak bu üç sorudan hangisinin hangi yanıtı vereceği görülebilir, ama kesin sayı yalnızca koşumdan okunur.
Özet
- Aynı iş üç biçimde yazılabilir: doğrudan çağrı, gecikmeli çağrılan kapanış, hiç çağrılmayan kapanış; kapanışlardan yalnız çağrılanlar koşuyor.
- Kursun ölçüm çekirdeğindeki
cagrilmayan-kapanisgövdesi bu koşumda hiç çalışmadı — kaynakta yazılı olmak koşmuş olmak anlamına gelmiyor. - Aynı kapanış birden çok kez çağrılırsa gövde her çağrıda yeniden koşuyor (üç çağrı üç koşum); yakaladığı değeri yalnızca okuyan bir kapanış istenildiği kadar çağrılabilirken, değeri gövdesinden dışarı taşıyan bir kapanış yalnızca bir kez çağrılabilir.
- Taşıma ile yakalanan bir değerin ömrü kapanışın kendisine bağlanıyor; gövde hiç koşmasa da yakalanan değer kapanışla birlikte bırakılıyor.
- Bir referansla yakalama, taşımadan farklı olarak kaynaktaki adı canlı tutuyor — taşımanın aksine değerin sahipliğini değiştirmiyor.
- Kapanışın tanımlanması koşum defterine girmeyen ayrı bir iştir; sayı yalnız gövde çağrılarını ölçüyor.
Sonraki Adım
Bu derste bir kapanışın gövdesi tek başına saklandı ve çağrılıp çağrılmamasına göre koştu
ya da koşmadı; kaç kez çağrıldığı da doğrudan gövdenin koşum sayısına yansıdı. Sıradaki
ders aynı soruyu tek bir kapanıştan bir zincire taşıyor: birçok kapanış aynı sözleşme
etrafında birleştiğinde, sözleşmenin kendisi — yineleyici — bu zincirin ne zaman
ilerleyeceğine karar veriyor. Kurulan bir zincirin, tek bir çağrıyla bile tetiklenmeden
koşum defterine hiç girmediği, bu dersteki cagrilmayan-kapanis örneğinin doğrudan bir
uzantısı olarak ölçülecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.