İçeriğe geç
academia.sh

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-kapanis gö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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat