İçeriğe geç
academia.sh

Ders 08 / 21

Kapanışlar

Kapanış sözdizimini, `$0` kısaltmasını, sondaki kapanışı ve `sorted(by:)` çağrısını tanıtır; yakalama listesinin kapanış oluşturulurken bir kez değerlendirildiğini ölçer.

Yayın Güncelleme

Bu derste

  • Kapanışı kısa sözdizimiyle, sondaki kapanış olarak yazmak
  • Kapanış gövdesinin her çağrıda çalıştığını, çağrılmayan kapanışın gövdesinin hiç çalışmadığını göstermek
  • Yakalama listesi ile listesiz yakalamanın farkını okumak
İçindekiler

Fonksiyonlar adlandırılmış davranıştır. Kapanış (closure) aynı davranışı adsız yazar ve bir değişkene koyabilir ya da başka bir fonksiyona verebilirsin. Bu dersin sorusu iki parçalı: kapanışın gövdesi ne zaman çalışır, ve kapanış çevresindeki değişkenleri hangi anda görür?

Kapanış Yazmak

En uzun biçim, parametre ve dönüş tipini süslü parantezin içine yazar: { (a: Int, b: Int) -> Bool in a < b }. in anahtar sözcüğü imzayı gövdeden ayırır. Tip bağlamdan çıkarılabildiğinde imza düşer ve parametrelere sıra numarasıyla erişilir: $0 ilk, $1 ikinci argümandır. Gövde tek bir ifadeyse return yazılmaz, ifadenin değeri örtük olarak döner.

Bir fonksiyonun son parametresi kapanışsa, kapanış parantezin dışına yazılabilir. Buna sondaki kapanış (trailing closure) denir. Parantez içinde başka argüman kalmadıysa parantezin kendisi de düşer: numbers.sorted { $0 > $1 }. sorted(by:) bir diziyi verdiğin karşılaştırma kapanışına göre sıralar.

// Probe.swift — bir ifadenin ne zaman degerlendirildigini kaydeder
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "hic" }
        if evaluations > 1 { return "her-kullanimda" }
        return evaluatedBeforeUse ? "hemen" : "ilk-kullanimda"
    }

    func report() {
        print("\(label): \(evaluations) degerlendirme, \(uses) kullanim -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["hemen", "ilk-kullanimda", "her-kullanimda", "hic"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}

Ölçüm bloğu önce bu biçimleri çalıştırır: uzun kapanış, $0 ile kısa kapanış, map ve kendi yazdığımız apply. Ardından iki kapanış kurar. scale gövdesinde bir Probe taşır ve iki kez çağrılır; unused aynı biçimde kurulur ama hiç çağrılmaz.

// main.swift — kapanis bicimleri ve govde ani
let numbers = [3, 1, 2]
print(numbers.sorted(by: { (a: Int, b: Int) -> Bool in a < b }))
print(numbers.sorted { $0 > $1 })
print(numbers.map { $0 * 2 })

func apply(_ n: Int, using transform: (Int) -> Int) -> Int { transform(n) }
print(apply(4) { $0 + 1 })

let bodyProbe = Probe("kapanis-govdesi")
let unusedProbe = Probe("cagrilmayan-govde")

let scale = { (n: Int) -> Int in bodyProbe.eval(n * 2) }
let unused = { (n: Int) -> Int in unusedProbe.eval(n) }

print(bodyProbe.use { scale(1) })
print(bodyProbe.use { scale(2) })
_ = unusedProbe.use { unused }
_ = unusedProbe.use { unused }

let allProbes = [bodyProbe, unusedProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
[1, 2, 3]
[3, 2, 1]
[6, 2, 4]
5
2
4
kapanis-govdesi: 2 degerlendirme, 2 kullanim -> her-kullanimda
cagrilmayan-govde: 0 degerlendirme, 2 kullanim -> hic
hemen: 0
ilk-kullanimda: 0
her-kullanimda: 1
hic: 1

sorted(by:) sonucu küçükten büyüğe, $0 > $1 ile büyükten küçüğe verdi. map her elemanı ikiye katladı ve apply(4) sondaki kapanışla 5 döndü. Ölçüm satırları kapanışın gövdesine bakar: kapanis-govdesi iki çağrıda iki kez değerlendirildi, yani gövde her çağrıda yeniden çalışır ve an her-kullanimda olur. cagrilmayan-govde ise iki kullanımda sıfır değerlendirme gösterdi: kapanışı kurmak gövdeyi çalıştırmaz, gövde yalnız çağrı olduğunda çalışır.

DT53. sorted(by:) karşılaştırma kapanışının kaç kez çağrıldığı bu derste sayılmadı; sayı sıralama algoritmasına bağlıdır ve ders yalnız sonucu gösterir.

DT54. Kapanış gövdesi yalnız Int döndüren, yan etkisi olmayan basit bir ifadeyle ölçüldü; kaçan (escaping) kapanışlar bu dersin dışındadır.

Kaçan Kapanış: Saklanan İş

Bir kapanışı fonksiyona verirsen ve fonksiyon kapanışı kendi bitişinden sonra da saklarsa, kapanışın tipi @escaping ile işaretlenmelidir. Bu işaret, kapanışın çağrıdan kaçabileceğini söyler. Kaçmayan kapanış (varsayılan) çağrı bitince ortadan kalkar. Kaçan kapanış ise bir diziye eklenebilir, bir özelliğe atanabilir ve sonra çağrılabilir. Kapanışı kuran satır işi yapmaz; işi, kapanış çağrıldığında yapılır.

Aşağıdaki blok bir kuyruk sınıfı yazar. add kapanışı saklar, runAll saklananların hepsini çağırır. Kapanışın gövdesi bir Probe’a yazar. Kapanışlar kuyruğa eklendikten sonra ve çalıştırıldıktan sonra değerlendirme sayısı yazdırılır.

// Queue.swift — saklanan kapanis
final class Queue {
    var jobs: [() -> Int] = []
    func add(_ job: @escaping () -> Int) {
        jobs.append(job)
    }
    func runAll() {
        for job in jobs { _ = job() }
    }
}
let queueProbe = Probe("saklanan-is")
let queue = Queue()
queue.add { queueProbe.eval(1) }
queue.add { queueProbe.eval(2) }
print(queueProbe.evaluations)
_ = queueProbe.use { queue.runAll() }
_ = queueProbe.use { queue.runAll() }
print(queueProbe.evaluations)
queueProbe.report()
0
4
saklanan-is: 4 degerlendirme, 2 kullanim -> her-kullanimda

İki kapanış kuyruğa eklendiğinde sayaç 0’dı: ekleme gövdeyi çalıştırmadı. İki runAll çağrısından sonra sayaç 4 oldu, çünkü her çağrıda iki kapanış çalıştı. Kapanışlar saklandı ve her çağrıldığında gövdeyi yeniden değerlendirdi; bu yüzden an her-kullanimda okunur.

Kaçan kapanış, işi ertelemenin yoludur. Gövdenin ne zaman çalışacağını kapanışı kuran satırdan değil, onu çağıran satırdan okursun.

DT55. Saklanan kapanışlar bir sınıf içinde ölçüldü; kaçan kapanışta self yakalamanın sonuçları bu dersin dışındadır.

Kısaltılmış Argüman Adları

Kapanış parametrelerine $0, $1 gibi adlar verebilirsin. Bu, tip bağlamdan çıkarıldığında parametre listesini yazmaktan kurtarır. Gövde tek bir ifadeyse örtük dönüş de çalışır. Standart kütüphane işleçleri de bir fonksiyon gibi geçilebilir: numbers.sorted(by: >) yazmak numbers.sorted { $0 > $1 } ile aynı işi yapar. reduce iki parametreli bir kapanış alır ve bu parametreler $0 ve $1 olur.

Aşağıdaki blok bir dizi üzerinde üç kısaltılmış biçimi çalıştırır. map her elemanı bir Probe’dan geçirir ve ikinci bir map çağrısı bu kez dört elemana uygulanır.

// Shorthand.swift — kisaltilmis argumanlar
let mapProbe = Probe("map-govdesi")
let numbers = [3, 1, 2]
print(numbers.sorted(by: >))
print(numbers.reduce(0) { $0 + $1 })
print(mapProbe.use { numbers.map { mapProbe.eval($0 * 10) } })
print(mapProbe.use { (numbers + [4]).map { mapProbe.eval($0 * 10) } })
mapProbe.report()
[3, 2, 1]
6
[30, 10, 20]
[30, 10, 20, 40]
map-govdesi: 7 degerlendirme, 2 kullanim -> her-kullanimda

sorted(by: >) [3, 2, 1] verdi. reduce toplamı 6 olarak hesapladı. İki map çağrısı [30, 10, 20] ve [30, 10, 20, 40] dizilerini üretti. Probe yedi değerlendirme gösterdi: ilk çağrıda üç, ikincisinde dört. Kapanış gövdesi her eleman için bir kez çalıştı. Bu yüzden an her-kullanimda okunur.

Kısaltılmış adlar gövde kısa olduğunda okunur; gövde uzadığında adlandırılmış parametre anlamı korur. $0 ve $1 yanında işleci doğrudan geçmek en kısa yazımdır ama yalnız işlecin imzası kapanışın imzasıyla eşleştiğinde çalışır.

DT56. Kısaltılmış adlar Int dizisiyle ölçüldü; $2 ve sonrası ile iç içe kapanışlarda $0 anlamı bu dersin dışındadır.

Kapanış Yakaladığı Değişkeni Değiştirebilir

Yakalanan değişken bir var ise kapanış onu yalnız okumakla kalmaz, değiştirebilir de. Kapanış değişkenin bir kopyasını değil, kendisini tutar. Aynı değişkeni birden çok kapanış yakalarsa, bir kapanışın yaptığı değişiklik diğerinin gördüğü değeri değiştirir. Bu yüzden yakalanan var’lar paylaşılan durum gibi davranır ve değişkenin ömrü, onu yakalayan kapanışlarla birlikte uzar.

Aşağıdaki blok bir hits sayacını iki kapanışla yakalar. record sayacı bir artırır ve değeri bir Probe’dan geçirir; skip sayacı doğrudan iki artırır. Kapanışlar sırayla record, skip, record biçiminde çağrılır.

// Shared.swift — ortak yakalanan degisken
let sharedProbe = Probe("ortak-sayac")
var hits = 0
let record = {
    hits += 1
    return sharedProbe.eval(hits)
}
let skip = { hits += 2 }
print(sharedProbe.use { record() })
skip()
print(sharedProbe.use { record() })
print(hits)
sharedProbe.report()
1
4
4
ortak-sayac: 2 degerlendirme, 2 kullanim -> her-kullanimda

record ilk çağrıda 1, skip sayacı 3 yaptıktan sonra ikinci çağrıda 4 döndürdü; son okuma hits değerinin 4 olduğunu doğruladı. İki kapanış aynı değişkeni gördü: record, skip’in yaptığı değişikliği ikinci çağrıda gördü. İki çağrı iki değerlendirme getirdi ve an her-kullanimda olur.

Paylaşılan durum esneklik sağlar, ama hangi kapanışın değişkeni ne zaman değiştirdiğini izlemeyi zorlaştırır. Durum küçük ve yakın olduğunda sorun olmaz; büyüdükçe durumu bir tipin içine almak daha okunur olur.

DT57. Paylaşılan durum bir Int üzerinde ölçüldü; eşzamanlı çalışan kapanışların aynı değişkene erişmesi bu dersin dışındadır.

Sondaki Kapanış Söz Dizimi

Bir fonksiyonun son parametresi kapanışsa, kapanışı parantezlerin dışına yazabilirsin. perform(times: 2) { ... } ile perform(times: 2, action: { ... }) aynı çağrıdır. Fonksiyonun tek argümanı kapanışsa, parantezler tümüyle kalkabilir. Bu söz dizimi yalnız yazımı kısaltır; kapanışın ne zaman çalıştığı değişmez. Kapanış, fonksiyon onu çağırdığında çalışır ve kaç kez çağırırsa o kadar kez çalışır.

Aşağıdaki blok bir fonksiyon yazar: perform verilen kapanışı istenen sayıda çağırır. Kapanış gövdesi bir Probe’a yazar. Fonksiyon önce iki, sonra üç tekrarla çağrılır.

// Trailing.swift — sondaki kapanis
func perform(times: Int, action: () -> Void) {
    for _ in 0..<times { action() }
}
let trailingProbe = Probe("sondaki-kapanis")
trailingProbe.use { perform(times: 2) { _ = trailingProbe.eval(1) } }
trailingProbe.use { perform(times: 3) { _ = trailingProbe.eval(1) } }
print(trailingProbe.evaluations)
trailingProbe.report()
5
sondaki-kapanis: 5 degerlendirme, 2 kullanim -> her-kullanimda

perform(times: 2) iki, perform(times: 3) üç kez çalıştırdı; toplam beş değerlendirme çıktı. Kapanışı parantezin dışına yazmak çalışma sayısını değiştirmedi: times değeri, kapanışın kaç kez çağrılacağını belirledi. Her çağrıda gövde yeniden değerlendirildi ve an her-kullanimda okunur.

Bu söz dizimi yalnız okunuşa yardım eder. Kapanış parametreler arasında son sıradaysa, çağrı bir kontrol akışı ifadesine benzer: perform(times: 3) { ... } bir döngü gibi okunur. Ama arkasındaki kural aynıdır: kapanış bir değerdir ve fonksiyon onu istediği kadar çağırır.

DT58. Sondaki kapanış dönüş değeri olmayan bir kapanışla ölçüldü; birden çok sondaki kapanışın söz dizimi bu dersin dışındadır.

Yakalama Listesi Değeri Dondurur

Bir kapanış, çevresindeki bir var değişkeni yakaladığında değişkenin kendisini tutar; sonradan değişkende yapılan değişiklikler kapanışa yansır. Bunu değiştirmek istersen kapanışın başına bir yakalama listesi yazarsın: { [snapshot = count] in snapshot }. Liste, kapanış oluşturulduğu anda count değerinin bir kopyasını alır ve kapanış artık onu kullanır. Sonradan count değişse bile kapanışın gördüğü sayı aynı kalır.

Aşağıdaki blok aynı değişkeni iki kapanışla yakalar: biri liste kullanmaz, biri kullanır. Kapanışlar oluşturulduktan sonra değişken değiştirilir ve ikisi de çağrılır. Okunan değer bir Probe üzerinden geçer.

// CaptureList.swift — yakalama listesi
let listProbe = Probe("yakalama-listesi")
var count = 1
let live = { listProbe.eval(count) }
let frozen = { [snapshot = count] in listProbe.eval(snapshot) }
count = 5
print(listProbe.use { live() }, listProbe.use { frozen() })
listProbe.report()
5 1
yakalama-listesi: 2 degerlendirme, 2 kullanim -> her-kullanimda

live kapanışı 5, frozen kapanışı 1 verdi. live değişkenin kendisini tuttuğu için count = 5 atamasından sonra yeni değeri gördü. frozen ise oluşturulduğu anda count değerinin bir kopyasını snapshot adıyla aldı; sonradan değişkeni değiştirmek ona ulaşmadı. İki çağrı iki değerlendirme getirdi ve an her-kullanimda olur.

Yakalama listesi, kapanışın hangi değeri göreceğini açıkça yazmanın yoludur. Bir değerin kapanış oluştuğu andaki hâliyle kalmasını istiyorsan listeye yazarsın; değişkenin sonradan alacağı değerleri görmesini istiyorsan listeyi hiç kullanmazsın.

Yakalama listesine birden çok kalem de yazabilirsin; her kalem, kapanış oluştuğu anda kendi kopyasını alır.

DT59. Yakalama listesi bir Int değişkenle ölçüldü; sınıf örneklerini zayıf yakalama bu dersin dışındadır ve sonraki konuda ele alınır.

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

Kapanış çevresindeki bir değişkeni kullanıyorsa onu yakalar (capture). Listesiz yakalamada kapanış değişkenin kendisine bağlanır: değişken sonradan değişirse kapanış her çağrıda yeni değeri görür. Yakalama listesi (capture list) ise köşeli parantez içinde yazılır ve kapanış oluşturulurken bir değer alır: { [snapshot = level] in snapshot }. Listedeki ifade kapanış kurulurken bir kez değerlendirilir, çağrılarda yeniden değerlendirilmez. Aşağıdaki ölçüm iki kapanışı aynı değişkenle kurar, değişkeni değiştirir ve ikisini de iki kez çağırır.

// main.swift — yakalama listesi
let captureProbe = Probe("yakalama-listesi")

var level = 1
let plain = { level }
let listed = { [snapshot = captureProbe.eval(level)] in snapshot }
level = 5

print(captureProbe.use { plain() }, captureProbe.use { listed() })
print(captureProbe.use { plain() }, captureProbe.use { listed() })

let allProbes = [captureProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
5 1
5 1
yakalama-listesi: 1 degerlendirme, 4 kullanim -> hemen
hemen: 1
ilk-kullanimda: 0
her-kullanimda: 0
hic: 0

plain kapanışı level değişkenine bağlı kaldığı için 5 gördü; listed kapanışı kurulduğu andaki 1 değerini taşıdı. Yakalama listesindeki ifade, kapanış kurulurken ve dört kullanımdan önce bir kez değerlendirildi: yakalama-listesi satırı hemen verir. Gövdenin her çağrıda çalışması ile yakalama listesinin bir kez çalışması aynı kapanışın iki ayrı anıdır.

DT60. level bir var’dır ve ölçüm yalnız değer tipi bir değişkenin yakalanmasını gösterir; sınıf örneklerinin yakalanması bu derste ölçülmedi.

DT61. Yakalama listesinin kapanış kurulurken bir kez çalışması tek bir kapanışla ölçüldü; aynı kapanışı birden çok yerde kuran kodlarda her kuruluşun ayrı bir değerlendirme olduğu bu dersin dışındadır.

Özet

  • Kapanış, adsız bir davranıştır; $0 ve $1 argümanlara sıra numarasıyla erişir, tek ifadelik gövde return gerektirmez.
  • Son parametre kapanışsa, kapanış parantezin dışına yazılabilir (sondaki kapanış).
  • Kapanış gövdesi her çağrıda çalışır; hiç çağrılmayan kapanışın gövdesi hiç çalışmaz.
  • Yakalama listesi kapanış kurulurken bir kez değerlendirilir; listesiz yakalanan değişkenin sonradan değişmesi çağrıda görünür.
  • Bu dersin anı: her kullanımda (kapanış gövdesi).

Sonraki Adım

Bu konudaki örnekler values.min() ?? 0 gibi bir ifade kullandı: min() boş bir dizi için değer döndüremez. Swift bu durumu bir değerin yokluğu olarak tipte taşır. Bir sonraki konunun ilk dersi, İsteğe Bağlı Değerler, bu fikri tanıtıyor.

İ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