Ders 18 / 21
Kapanışlarda Yakalama
Bir nesnenin kendi özelliğinde sakladığı kapanışın self'i yakalayınca deinit'i engellediğini, weak self yakalama listesiyle çözüldüğünü ve yakalama listesinin kapanış kurulurken bir kez değerlendirildiğini ölçer.
Yayın Güncelleme
Bu derste
- Özellikte saklanan bir kapanışın self'i güçlü yakalayınca tutma döngüsü kurduğunu deinit çıktısından okur
- Weak self yakalama listesiyle döngüyü kırar ve kapanışın nesne gittikten sonra boş çalıştığını gözler
- Yakalama listesinin kapanış kurulurken bir kez değerlendirildiğini, listesiz yakalamanın her çağrıda güncel değeri okuduğunu ölçer
- Saklanmayan bir kapanışın self kullanmasının döngü kurmadığını ayırt eder
İçindekiler
Önceki ders tutma döngüsünü iki sınıf özelliği arasında kurdu. Aynı döngü bir sınıfın kendi özelliğinde sakladığı kapanış üzerinden de kurulur ve pratikte en sık rastlanan biçim budur. Kapanış, gövdesinde kullandığı dış değerleri kendi içine alır; buna yakalama denir. Kapanışın gövdesi self’i kullanıyorsa kapanış örneği güçlü referansla yakalar. Örnek de kapanışı bir özelliğinde tutuyorsa iki taraf birbirini tutar.
Bu ders döngüyü bir kez kurar, [weak self] yakalama listesiyle kırar ve ardından yakalama listesinin başka bir özelliğini ölçer: listenin içindeki ifade kapanışın her çağrısında değil, kapanış kurulurken bir kez değerlendirilir. Sonunda bu sorunun sınırını çizen bir ölçüm gelir: her kapanış döngü kurmaz.
// 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)")
}
}
Kapanış Örneği Tutarsa
Button sınıfı onTap adında isteğe bağlı bir kapanış saklar. install yöntemi bu özelliğe, gövdesinde self.name okuyan bir kapanış atar. Her turda bir Button kurulur, kapanışı yüklenir, bir kez çağrılır ve ad bırakılır.
// main.swift — kapanis self'i guclu yakalayinca deinit kosmaz
final class Button {
let name: String
let owner: Probe
var onTap: (() -> Void)?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
func install() {
onTap = { print("dokundu \(self.name)") }
}
deinit {
print("deinit dugme \(name)")
_ = owner.eval(name)
}
}
let trapped = Probe("kapanis-dongusu")
for round in 1...2 {
var button: Button? = Button("dugme-\(round)", owner: trapped)
button?.install()
button?.onTap?()
_ = trapped.use { button = nil }
print("birakildi \(round): \(trapped.evaluations) deinit")
}
trapped.report()
dokundu dugme-1 birakildi 1: 0 deinit dokundu dugme-2 birakildi 2: 0 deinit kapanis-dongusu: 0 degerlendirme, 2 kullanim -> hic
Kapanış çağrıldı ve ad yazıldı, ama bırakmadan sonra deinit satırı hiç görünmez. Button kapanışı onTap ile tutuyor, kapanış da self üzerinden Button’ı tutuyor. Dışarıdaki ad gittiğinde sayaç bire düşer, sıfıra inmez. Önceki dersin kuralı burada da geçerli: kapanis-dongusu iki kullanım ve sıfır değerlendirme kaydeder, anı hic olur.
Weak Self ile Kırmak
Çözüm kapanışın başına bir yakalama listesi koymaktır. [weak self] listesi kapanışa self’in zayıf bir kopyasını verir; kapanış artık örneği tutmaz ama örnek gidince self isteğe bağlı olur ve nil olabilir. Gövde bu yüzden guard let self ile önce örneğin yaşayıp yaşamadığına bakar. Ölçüm kapanışı iki kez çağırır: ad bırakılmadan önce ve bırakıldıktan sonra. Bırakılma sonrası çağrı için kapanış ayrı bir değişkende saklanır.
// main.swift — [weak self] dongunun onune gecer
final class Button {
let name: String
let owner: Probe
var onTap: (() -> Void)?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
func install() {
onTap = { [weak self] in
guard let self else {
print("dugme yok")
return
}
print("dokundu \(self.name)")
}
}
deinit {
print("deinit dugme \(name)")
_ = owner.eval(name)
}
}
let broken = Probe("zayif-yakalama")
for round in 1...2 {
var button: Button? = Button("dugme-\(round)", owner: broken)
button?.install()
let saved = button?.onTap
saved?()
_ = broken.use { button = nil }
saved?()
print("birakildi \(round): \(broken.evaluations) deinit")
}
broken.report()
dokundu dugme-1 deinit dugme dugme-1 dugme yok birakildi 1: 1 deinit dokundu dugme-2 deinit dugme dugme-2 dugme yok birakildi 2: 2 deinit zayif-yakalama: 2 degerlendirme, 2 kullanim -> her-kullanimda
Bu kez her turda deinit satırı bırakmanın hemen ardından çıkar. Kapanış Button’ı tutmuyordu, bu yüzden dışarıdaki ad gidince sayaç sıfıra indi. Bırakmadan sonra aynı kapanış hâlâ çağrılabilir ama guard let self başarısız olur ve dugme yok yazılır. Kapanış artık boş çalışıyor: örnek gitti, kapanış kaldı. zayif-yakalama iki kullanım ve iki değerlendirme kaydeder; bu iki tur boyunca her kullanımda bir deinit koşar ve anı her-kullanimda olur.
Yakalama Listesi Ne Zaman Değerlendirilir
[weak self] yakalama listesinin yalnız bir kullanımıdır. Liste aslında kapanışa bir değer de verebilir: [snapshot = ifade] biçiminde yazılan her giriş, kapanış kurulurken bir kez değerlendirilir ve kapanışın içinde sabit bir kopya olur. Listesiz yakalanan bir değişken ise kapanışın her çağrısında güncel değeriyle okunur. Aşağıdaki ölçüm bu farkı iki Probe ile gösterir. Önce ikisi de level değerini 1 iken kurulur, sonra level 5 olur, sonra her kapanış iki kez çağrılır.
// main.swift — yakalama listesi kurulurken, listesiz okuma her cagrida
let listed = Probe("yakalama-listesi")
let live = Probe("listesiz-yakalama")
do {
var level = 1
let withList = { [snapshot = listed.eval(level)] in snapshot }
let withoutList = { live.eval(level) }
level = 5
print("listeli: \(listed.use { withList() }) \(listed.use { withList() })")
print("listesiz: \(live.use { withoutList() }) \(live.use { withoutList() })")
}
listed.report()
live.report()
listeli: 1 1 listesiz: 5 5 yakalama-listesi: 1 degerlendirme, 2 kullanim -> hemen listesiz-yakalama: 2 degerlendirme, 2 kullanim -> her-kullanimda
Listeli kapanış iki çağrıda da 1 döndürür: kopya level hâlâ 1 iken alınmıştı ve sonraki atama onu değiştirmedi. Listesiz kapanış iki çağrıda da 5 döndürür, çünkü değişkeni her çağrıda yeniden okur. Probe çıktısı bunu iki farklı an olarak kaydeder. yakalama-listesi tek değerlendirme kaydeder ve bu değerlendirme ilk kullanımdan önce olduğu için anı hemendir. listesiz-yakalama iki değerlendirme kaydeder, bu yüzden her-kullanimda olur. Bu dersin anı hemendir: yakalama listesindeki ifade kapanış kurulduğu noktada bir kez değerlendirilir.
İki Kapanış Aynı Değişkeni Paylaşır
Yakalama bir değerin kopyalanması değil, çoğu zaman bir değişkenin paylaşılmasıdır. Aynı yerel değişkeni yakalayan iki kapanış, o değişkenin aynı kutusuna bakar. Bu ölçüm bir total değişkenini yakalayan bir artırıcı ile bir okuyucu kapanış kurar. Artırıcı her çağrıda Probe üzerinden bir değerlendirme yapar ve okuyucu, artırıcının yaptığı değişikliği görür mü, buna bakar.
// main.swift — iki kapanis ayni degiskeni yakalar
let shared = Probe("ortak-degisken")
do {
var total = 0
let add = { total += shared.eval(10) }
let read = { total }
print("baslangic \(read())")
shared.use { add() }
print("birinci artis \(read())")
shared.use { add() }
print("ikinci artis \(read())")
}
shared.report()
baslangic 0 birinci artis 10 ikinci artis 20 ortak-degisken: 2 degerlendirme, 2 kullanim -> her-kullanimda
read kapanışı artırıcının yaptığı değişikliği görür: baslangic 0, ardından birinci artis 10 ve ikinci artis 20. İki kapanış total değerinin ayrı kopyalarını tutsaydı okuyucu hep 0 verirdi. Kapanışlar aynı kutuya bakar ve kutuyu değiştiren her çağrı öbürüne görünür. ortak-degisken iki kullanımda iki değerlendirme sayar ve her-kullanimda der. Önceki bölümdeki yakalama listesi bu paylaşımı keser: [total] yazılan bir kapanış, değişkeni değil o andaki değeri alır. Paylaşım varsayılandır; kopya ise açıkça istenen bir davranıştır.
Kaçan ve Kaçmayan Kapanış
Bir kapanış, kendisini alan işlevden sonra da yaşayabilir. Yaşayacaksa parametresi @escaping olarak işaretlenir; işaretlenmemiş kapanış işlev dönmeden bitmek zorundadır. Bu iki biçim Probe ile ölçülür. Kaçmayan kapanış run işlevinde hemen çağrılır. Kaçan kapanış store işlevinde bir diziye eklenir ve ancak sonra çağrılır. İki store çağrısından sonra, kapanışlar çağrılmadan önce later.evaluations okunur.
// main.swift — kacmayan hemen, kacan sonra degerlendirilir
let now = Probe("kacmayan")
let later = Probe("kacan")
do {
var pending: [() -> Int] = []
func run(_ work: () -> Int) -> Int {
work()
}
func store(_ work: @escaping () -> Int) {
pending.append(work)
}
for round in 1...2 {
_ = now.use { run { now.eval(round) } }
store { later.eval(round) }
print("tur \(round): kacmayan \(now.evaluations), kacan \(later.evaluations)")
}
for work in pending {
_ = later.use { work() }
}
print("cagrildiktan sonra kacan \(later.evaluations)")
}
now.report()
later.report()
tur 1: kacmayan 1, kacan 0 tur 2: kacmayan 2, kacan 0 cagrildiktan sonra kacan 2 kacmayan: 2 degerlendirme, 2 kullanim -> her-kullanimda kacan: 2 degerlendirme, 2 kullanim -> her-kullanimda
Her turda kacmayan sayacı run dönmeden önce artar: ilk turda 1, ikinci turda 2. kacan sayacı ise iki store çağrısından sonra da 0’dır, çünkü store kapanışı yalnız diziye ekler ve çağırmaz. Kapanışlar ancak sonraki döngüde çağrılır ve sayaç cagrildiktan sonra kacan 2 olur. İki Probe de her-kullanimda der, ama değerlendirmenin yeri farklıdır: kaçmayan kapanışın değerlendirmesi, onu alan işlevin çağrısı içindedir. Kaçan kapanışın değerlendirmesi, kapanışın kurulduğu satırdan çok sonra gelebilir. Bu fark bir kapanışın neden self’i açıkça yazmaya zorladığını da açıklar: kaçan kapanış, yakaladığı örnekten uzun yaşayabilir. Kaçmayan kapanış ise örneği hiçbir zaman tutamaz, çünkü işlev dönünce biter.
Güçlü Geçici Kopya
guard let self satırı yalnız self’in nil olup olmadığına bakmaz; gövde boyunca yaşayan güçlü bir geçici referans da kurar. Bu referans, kapanış bitene kadar örneği tutar. Ölçüm bunu şöyle sınar: Job örneğinin kapanışı, gövdenin ortasında dışarıdaki adı bırakan bir işlevi çağırır. deinit bırakılma satırında mı, yoksa gövde bittikten sonra mı koşar?
// main.swift — guard let self govde boyunca ornegi tutar
final class Job {
let name: String
let owner: Probe
var run: (() -> Void)?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
func install(release: @escaping () -> Void) {
run = { [weak self] in
guard let self else {
print("is yok")
return
}
print("govde basladi \(self.name)")
release()
print("govde bitiyor \(self.name)")
}
}
deinit {
print("deinit is \(name)")
_ = owner.eval(name)
}
}
let held = Probe("gecici-guclu")
for round in 1...2 {
var job: Job? = Job("is-\(round)", owner: held)
job?.install { job = nil }
let saved = job?.run
_ = held.use { saved?() }
print("kapanis dondu: \(held.evaluations) deinit")
}
held.report()
govde basladi is-1 govde bitiyor is-1 deinit is is-1 kapanis dondu: 1 deinit govde basladi is-2 govde bitiyor is-2 deinit is is-2 kapanis dondu: 2 deinit gecici-guclu: 2 degerlendirme, 2 kullanim -> her-kullanimda
Her turda govde basladi ve govde bitiyor satırları arasında release() çağrılır ve dışarıdaki job adı nil olur. Yine de deinit is is-1 satırı govde bitiyor is-1 satırından sonra gelir. guard let self ile kurulan geçici güçlü referans, gövde bitene kadar örneği ayakta tutar; son referans ancak kapanış dönerken bırakılır. kapanis dondu: 1 deinit satırı bunu doğrular: kapanış döndüğü anda sayaç 1’dir. gecici-guclu iki turda iki değerlendirme sayar ve her-kullanimda der. Bunun pratik sonucu iki yönlüdür. [weak self] kapanışın örneği kalıcı olarak tutmasını önler, ama gövde çalışırken örnek ortadan kalkmaz. Gövdede yapılan işlem örneğin kaybolacağı korkusuyla bölünmek zorunda değildir.
Sınırlayıcı Ölçüm
Her kapanış döngü kurmaz. Döngü için kapanışın örnekte saklanması gerekir. Bir yöntemin içinde kurulup yöntem dönmeden biten bir kapanış, örneğin bir dizi dönüştürmesine verilen kapanış, hiçbir yerde saklanmaz ve self’i yakalaması örneği tutmaz. Ölçüm bir Report örneğinde bunu gösterir: joined yöntemi parts.map { ... } içinde name özelliğini okur, sonuç döner ve örnek bırakılır.
// main.swift — saklanmayan kapanis dongu kurmaz
final class Report {
let name: String
let owner: Probe
let parts = ["a", "b", "c"]
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
func joined() -> String {
parts.map { "\($0)-\(name)" }.joined(separator: ",")
}
deinit {
print("deinit rapor \(name)")
_ = owner.eval(name)
}
}
let passing = Probe("gecici-kapanis")
for round in 1...2 {
var report: Report? = Report("rapor-\(round)", owner: passing)
print(report?.joined() ?? "yok")
_ = passing.use { report = nil }
print("birakildi \(round): \(passing.evaluations) deinit")
}
passing.report()
a-rapor-1,b-rapor-1,c-rapor-1 deinit rapor rapor-1 birakildi 1: 1 deinit a-rapor-2,b-rapor-2,c-rapor-2 deinit rapor rapor-2 birakildi 2: 2 deinit gecici-kapanis: 2 degerlendirme, 2 kullanim -> her-kullanimda
Her turda deinit bırakma anında koşar. map kapanışı self’i okudu ama yöntem döndüğü anda kapanış da bitti; ona bir referans kalmadı. Sızıntı yakalamanın kendisinden değil, yakalayan kapanışın örnekte yaşamaya devam etmesinden doğar. Yakalama listesi gerektiren yer yalnızca saklanan kapanıştır.
BL3. Ölçülen her sınıf kapanışı tek bir özellikte saklıyor. Birden çok saklanan kapanışın ya da kapanışı dışarıdaki bir listede tutan nesnenin aynı kuralları izlediği bu derste ölçülmedi. Kapanış sayısının bir olması tasarım tercihidir.
Özet
- Özellikte saklanan bir kapanış
self’i güçlü yakalarsa örnek ile kapanış birbirini tutar vedeinithiç koşmaz. [weak self]yakalama listesi örneği tutmaz; örnek gidinceselfnilolur veguard let selfkapanışı boş çalıştırır.- Yakalama listesindeki
[snapshot = ifade]kapanış kurulurken bir kez değerlendirilir; listesiz yakalanan değişken her çağrıda güncel değeriyle okunur. - Saklanmayan bir kapanış, örneğin
mapiçindekiler,self’i kullansa da döngü kurmaz. - Bu dersin anı: hemen. Yakalama listesindeki ifade kapanışın kurulduğu noktada bir kez değerlendirilir.
Sonraki Adım
Bu derste kapanış, bir değeri ya da örneği kendi içine alıp saklayan bir kutuydu. Özellik sarmalayıcıları da benzer bir şeyi bir özelliğin kendisi için yapar: değerin nasıl saklanacağını ve okunacağını ayrı bir tipe taşır. Sonraki ders wrappedValue okuyucusunun ne zaman çalıştığını ve sarmalayıcının başlangıç değerinin ne zaman kurulduğunu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.