İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

Swift Closures Kullanımı: $0, @escaping ve [weak self]

Ahmet Balaman

5 dk okuma

SwiftClosuresEscapingWeak SelfSwiftUIiOS
Swift Closures Kullanımı: $0, @escaping ve [weak self]

Closure, bir değişkene atanabilen, fonksiyona parametre olarak verilebilen isimsiz bir kod bloğudur. Swift'te sorted, map, filter, ağ isteklerinin tamamlanma blokları ve SwiftUI'daki her Button aksiyonu closure'dır; yani dili kullanıp closure'dan kaçmak mümkün değil. Yeni başlayanları zorlayan şey kavramın kendisi değil, aynı şeyin altı farklı şekilde yazılabilmesidir. Bu yazıda en uzun yazımdan en kısasına adım adım iniyor, ardından bellek tarafına, yani @escaping ve [weak self] konularına geçiyoruz.

Fonksiyondan Closure'a

Bir fonksiyon ile closure arasındaki fark yalnızca isim ve yazım biçimidir:

func greet(name: String) -> String {
    return "Merhaba, \(name)"
}

let greetClosure = { (name: String) -> String in
    return "Merhaba, \(name)"
}

print(greet(name: "Ada"))      // Merhaba, Ada
print(greetClosure("Ada"))     // Merhaba, Ada

Closure'da parametreler ve dönüş tipi süslü parantezin içine taşınır, in kelimesi de imza ile gövdeyi ayırır. greetClosure'ın tipi (String) -> String'dir. Çağırırken argüman etiketi yazılmadığına dikkat edin.

Tam Formdan $0'a Altı Adım

Aynı sıralama işlemini giderek kısaltalım:

let numbers = [4, 1, 7, 3]

// 1. Tam form
let s1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
    return a < b
})

// 2. Tipler bağlamdan çıkarılır
let s2 = numbers.sorted(by: { a, b in return a < b })

// 3. Tek ifadede return yazılmaz
let s3 = numbers.sorted(by: { a, b in a < b })

// 4. Kısa argüman adları
let s4 = numbers.sorted(by: { $0 < $1 })

// 5. Trailing closure
let s5 = numbers.sorted { $0 < $1 }

// 6. Operatörün kendisi de bir fonksiyondur
let s6 = numbers.sorted(by: <)

Altısı da [1, 3, 4, 7] üretir. Derleyici sorted(by:)'ın (Int, Int) -> Bool beklediğini bildiği için tipleri yazmanıza gerek kalmaz. $0 ilk, $1 ikinci parametredir.

Hangisini seçmeli? Gövde tek kısa ifadeyse $0 okunaklıdır. Gövde birkaç satırsa ya da iç içe closure varsa parametreye isim verin; iç içe closure'larda $0 en içteki closure'a aittir ve hangisinden bahsettiğiniz hızla karışır.

Trailing Closure

Fonksiyonun son parametresi closure ise, closure parantezin dışına yazılabilir:

func repeatTask(times: Int, task: (Int) -> Void) {
    for index in 1...times {
        task(index)
    }
}

repeatTask(times: 3) { index in
    print("\(index). tekrar")
}

SwiftUI kodunun bu kadar temiz görünmesinin sebebi budur: VStack { … }, Button("Kaydet") { … }, List { … } hep trailing closure'dır.

Değer Yakalama

Closure, tanımlandığı yerdeki değişkenleri yakalar ve o kapsam bittikten sonra da yaşatır:

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let next = makeCounter()
print(next())   // 1
print(next())   // 2

let another = makeCounter()
print(another())   // 1

makeCounter bittiğinde count normalde yok olmalıydı. Ama dönen closure onu yakaladığı için yaşamaya devam ediyor. another kendi ayrı count'unu alıyor.

Önemli ayrıntı: closure değişkenin o anki değerini değil, değişkenin kendisini yakalar. Değeri dondurmak isterseniz capture list kullanılır:

var level = 1
let printLive = { print("Seviye: \(level)") }
let printFrozen = { [level] in print("Seviye: \(level)") }

level = 5
printLive()     // Seviye: 5
printFrozen()   // Seviye: 1

Closure'lar referans tipidir. Bir closure'ı iki değişkene atarsanız ikisi aynı yakalanmış durumu paylaşır. Değer ve referans tipi ayrımını struct ve class farkı yazısında ayrıntılı anlattım.

@escaping: Fonksiyondan Sonra Yaşayan Closure

Varsayılan olarak parametre closure'ı non-escaping'dir: fonksiyon dönmeden önce çağrılır ve işi biter. Closure fonksiyon bittikten sonra çağrılacaksa, yani bir özelliğe saklanıyor ya da asenkron bir işe veriliyorsa, @escaping ile işaretlenmesi gerekir:

import Foundation

func fetchGreeting(completion: @escaping (String) -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion("Merhaba")
    }
}

fetchGreeting { text in
    print(text)   // 1 saniye sonra: Merhaba
}

@escaping'i silerseniz derleyici Escaping closure captures non-escaping parameter 'completion' hatası verir. Bu işaret bir formalite değildir; "bu closure yakaladığı şeyleri uzun süre tutabilir, belleği düşün" uyarısıdır. Aynı nedenle class içindeki escaping closure'larda self. yazmanız istenir; yazmazsanız reference to property … in closure requires explicit use of 'self' to make capture semantics explicit hatası alırsınız.

Retain Cycle ve [weak self]

Sorun şöyle oluşur: nesne closure'ı bir özelliğinde tutar, closure da self'i yakalar. İkisi birbirini güçlü referansla tuttuğu için hiçbiri bellekten silinmez.

final class Stopwatch {
    var seconds = 0
    var onTick: (() -> Void)?

    deinit {
        print("Stopwatch bellekten silindi")
    }

    func start() {
        onTick = {
            self.seconds += 1      // self -> onTick -> self
        }
    }
}

var stopwatch: Stopwatch? = Stopwatch()
stopwatch?.start()
stopwatch = nil     // deinit mesajı gelmez

Çözüm, self'i zayıf yakalamaktır:

func start() {
    onTick = { [weak self] in
        guard let self else { return }
        self.seconds += 1
    }
}

[weak self] ile self closure içinde opsiyonel olur; nesne silinmişse nil gelir. guard let self else { return } kalıbı bu opsiyoneli açmanın en temiz yoludur; mantığı guard let yazısındakiyle aynıdır. Bu değişiklikten sonra stopwatch = nil dediğinizde deinit mesajını görürsünüz.

[unowned self] de bir seçenektir ama nesne silindikten sonra closure çalışırsa uygulama çöker. Closure'ın nesneden uzun yaşamayacağından emin değilseniz weak kullanın.

Her closure'a [weak self] yazmak gerekmez. map, filter, sorted gibi non-escaping closure'lar fonksiyonla birlikte biter, döngü oluşturamaz.

map, filter, sorted ve reduce

Closure'ların en keyifli kullanım alanı koleksiyonlardır:

struct Student {
    let name: String
    let grade: Int
}

let students = [
    Student(name: "Ada", grade: 85),
    Student(name: "Linus", grade: 42),
    Student(name: "Grace", grade: 97),
    Student(name: "Alan", grade: 68)
]

let passedNames = students
    .filter { $0.grade >= 50 }
    .sorted { $0.grade > $1.grade }
    .map { $0.name }

print(passedNames)   // ["Grace", "Ada", "Alan"]

let total = students.reduce(0) { sum, student in sum + student.grade }
let average = Double(total) / Double(students.count)
print(average)       // 73.0

filter koşulu sağlayanları seçer, sorted sıralar, map her elemanı başka bir şeye dönüştürür, reduce hepsini tek değere indirir. reduce'ta iki parametrenin anlamı farklı olduğu için isim vermek $0 + $1.grade yazmaktan daha okunaklıdır. Sadece bir özelliği çekiyorsanız key path daha da kısadır: students.map(\.grade).

SwiftUI Buton Aksiyonları

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("\(count)")
                .font(.largeTitle)

            Button("Artır") {
                count += 1
            }

            Button(action: { count = 0 }) {
                Label("Sıfırla", systemImage: "arrow.counterclockwise")
            }
        }
    }
}

İlk butonda aksiyon trailing closure olarak verildi. İkincisinde iki closure var: action ve görünümü üreten label. SwiftUI view'ları struct olduğu için burada [weak self] yazılmaz; denerseniz 'weak' may only be applied to class and class-bound protocol types hatası alırsınız. Retain cycle riski view'da değil, view'ın kullandığı class tabanlı model nesnelerinde ortaya çıkar. Bu yapının tamamını sıfırdan görmek için SwiftUI ile ilk uygulama yazısına bakabilirsiniz.

Gerçekçi Senaryo: Arama Ekranının Modeli

import Foundation

final class SearchViewModel {
    private(set) var results: [String] = []
    var onResultsChanged: (([String]) -> Void)?

    private let allItems = ["Swift", "SwiftUI", "Xcode", "Flutter", "Dart"]

    func search(_ query: String) {
        loadItems(matching: query) { [weak self] found in
            guard let self else { return }
            self.results = found
            self.onResultsChanged?(found)
        }
    }

    private func loadItems(matching query: String,
                           completion: @escaping ([String]) -> Void) {
        let items = allItems
        DispatchQueue.global().async {
            let found = items.filter { $0.localizedCaseInsensitiveContains(query) }
            DispatchQueue.main.async {
                completion(found)
            }
        }
    }
}

let viewModel = SearchViewModel()
viewModel.onResultsChanged = { print("Sonuçlar:", $0) }
viewModel.search("swift")   // Sonuçlar: ["Swift", "SwiftUI"]

Bu küçük sınıfta yazıdaki her şey var. completion arka plan kuyruğuna verildiği için @escaping. Kullanıcı sonuç gelmeden ekranı kapatabilir; [weak self] sayesinde model bellekte asılı kalmaz, closure sessizce çıkar. filter içindeki closure non-escaping olduğu için $0 ile kısa tutuldu. Arka plan closure'ına self yerine items kopyasını vermek de bilinçli bir tercih: dizi değer tipi olduğu için kuyruklar arasında güvenle taşınır.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

Closure şu durumlarda doğru araçtır: bir davranışı parametre olarak vermek (sorted, filter), kısa olay tepkileri (buton aksiyonu), bir nesnenin dışarıya tek bir olay bildirmesi (onResultsChanged).

Şu durumlarda alternatife bakın:

  • Asenkron adımlar iç içe girmeye başladıysa, yani completion içinde completion varsa, async/await çok daha okunaklıdır. Geçişi async/await rehberinde anlattım.
  • Closure gövdesi onlarca satıra ulaştıysa onu isimli bir metoda taşıyın ve metodu referans olarak verin: Button("Ekle", action: addItem).
  • Bir nesne dışarıya üç dört farklı olay bildiriyorsa, ayrı ayrı closure özellikleri yerine bir protocol (delegate) daha düzenli olur.

Sık Yapılan Hatalar

1. @escaping yazmayı unutmak

Belirti: Escaping closure captures non-escaping parameter 'completion' ya da closure'ı bir özelliğe atarken assigning non-escaping parameter … to an '@escaping' closure. Çözüm: parametre tipinin önüne @escaping eklemek.

2. Saklanan closure'da self'i güçlü yakalamak

Belirti: derleme hatası yok, ama ekran kapatıldığı hâlde deinit hiç çalışmıyor, zamanlayıcılar ve dinleyiciler arkada devam ediyor. Çözüm: [weak self] ve guard let self else { return }. Şüphelendiğiniz sınıfa geçici bir deinit { print(…) } eklemek en hızlı teşhis yöntemidir.

3. Değerin dondurulduğunu sanmak

Belirti: closure, tanımlandığı andaki değil, çalıştığı andaki değeri yazdırıyor. Closure değişkeni yakalar, değeri değil. Çözüm: { [level] in … } şeklinde capture list kullanmak.

4. İç içe closure'larda $0 kullanmak

Belirti: $0'ın dıştaki elemanı gösterdiğini sanıyorsunuz ama içteki closure'ın parametresini gösteriyor; sonuç sessizce yanlış çıkıyor ya da tip hatası alıyorsunuz. Çözüm: dıştaki closure'da parametreye isim vermek. let matrix = [[1, 2], [3, 4]] için matrix.map { row in row.filter { $0 > row[0] } } yazdığınızda row dıştaki, $0 içteki elemandır ve karışmaz.

Sık Sorulan Sorular

Closure ile fonksiyon arasındaki fark nedir?

Fonksiyonlar aslında isim verilmiş closure'lardır. Closure isimsizdir, yerinde yazılır ve çevresindeki değişkenleri yakalayabilir; tipi ikisinde de (Parametreler) -> DönüşTipi biçimindedir.

$0 ne anlama gelir?

Closure'ın ilk parametresinin otomatik adıdır; ikincisi $1, üçüncüsü $2 olur. Parametrelere in ile isim vermediğinizde kullanılabilir ve kısa, tek ifadeli closure'larda tercih edilir.

Her closure'da [weak self] yazmalı mıyım?

Hayır. Yalnızca closure saklanıyor ya da uzun süre yaşıyorsa ve self bir class örneğiyse gerekir. map, filter gibi non-escaping closure'larda ve struct olan SwiftUI view'larında gerek yoktur.

@escaping ne zaman gerekir?

Closure, verildiği fonksiyon döndükten sonra çağrılacaksa: bir özelliğe ya da diziye saklanıyorsa veya asenkron bir işe aktarılıyorsa. Fonksiyon içinde hemen çağrılıp biten closure'lar için gerekmez.

Yorumlar