Swift: Java’dan Gelen Birinin Gözünden
2021’de Java’dan Swift’e geçtim ve ilk haftalarda yazdığım şey aslında Swift söz dizimiyle yazılmış Java’ydı. Optional’lar, değer tipleri, protokol uzantılarındaki gönderim davranışı, ARC ve closure yakalama listeleri konusunda hangi refleksimin nerede yanlış çalıştığını, kendi hatalarım üzerinden anlatıyorum.
2020’de Java ile Android uygulamaları yazıyordum, 2021’de Swift’e geçtim. İlk haftalarda yazdığım şey aslında Swift değildi; Swift söz dizimiyle yazılmış Java’ydı. Derleyici çoğu yerde beni durdurdu, durdurmadığı yerlerde uygulama çalışma zamanında çöktü. Bu yazı, o geçişte hangi Java refleksimin nerede yanlış çalıştığının kaydı.
Optional, null kontrolünün süslü hâli değil
Java’da null her referansın sessiz bir olasılığıdır. Tip imzasına bakarak bir değerin boş gelip gelmeyeceğini anlayamazsın; bunu ya dokümandan ya da NullPointerException’dan öğrenirsin. Swift’te ise String ile String? iki ayrı tiptir. İkincisi aslında bir enum: ya .some(deger) ya da .none. Bu yüzden derleyici, boş olabilecek bir değeri açmadan kullanmana izin vermez.
İlk refleksim ünlem işaretiydi. Java’da “burası kesin dolu” diye düşünüp kontrolü atlamaya alışmıştım; Swift’te aynı düşünceyi ! ile yazdım ve uygulama “Unexpectedly found nil while unwrapping an Optional value” diyerek kapandı. Zorla açma kontrolü kaldırmıyor; kontrolü çalışma zamanına erteleyip hata durumunda çökmeyi seçiyor.
// Java tarafındaki alışkanlık
String ad = kullanici.getAd();
if (ad != null && !ad.isEmpty()) { ... }
// Swift'teki karşılığı
guard let ad = kullanici.ad, !ad.isEmpty else { return }
// buradan sonra ad, Optional değil düz String
Bugün neredeyse her yerde guard let kullanıyorum. Sebep estetik değil: guard erken çıkışı zorunlu kıldığı için açılan değer aynı kapsamda kullanılabiliyor ve kod iç içe if bloklarına dönüşmüyor. Zincirleme erişim için ?., varsayılan değer için ?? çoğu durumda yetiyor.
Tek makul istisna Storyboard bağlantıları oldu: @IBOutlet özellikleri ! ile tanımlanır, çünkü nesne init sırasında değil arayüz yüklenirken bağlanır. Bunu görüp “demek ki ünlem normal bir şeymiş” diye genellemek benim ilk aylardaki hatamdı.
! ve try!, “bu asla olmayacak” demenin kod hâlidir. Olduğunda kullanıcının elinde uygulama kapanır. Ben bunları yalnızca karşılanmadığı anda zaten devam edilemeyecek varsayımlar için bırakıyorum; kalan her yerde açık bir hata yolu yazıyorum.
Değer tipi ile referans tipi
Java’da ilkel tipler dışında her şey referanstır. Swift’te struct, enum ve tuple değer tipidir; atandığında veya fonksiyona geçirildiğinde kopyalanır. Array, Dictionary ve String de struct’tır. Bir diziyi fonksiyona verip içeride değiştirdiğinde çağıran tarafın elindeki dizi değişmez; Java’dan gelen biri için en şaşırtıcı davranış bu.
let de Java’daki final ile birebir aynı değil. Bir sınıf örneğini let ile tuttuğunda sabitlenen şey referanstır, içerik değil. Bir struct’ı let ile tuttuğunda ise her şey sabitlenir:
struct Sepet { var urunler: [String] = [] }
let sepet = Sepet()
// sepet.urunler.append("kalem") // derlenmez: struct tamamen sabit
final class SepetRef { var urunler: [String] = [] }
let sepetRef = SepetRef()
sepetRef.urunler.append("kalem") // derlenir: referans sabit, içerik değil
Aynı sebeple bir struct’ın kendi alanlarını değiştiren metodunu mutating olarak işaretlemek gerekiyor. Kopyalama maliyeti ilk aklıma gelen soruydu; standart kütüphane koleksiyonları copy-on-write ile çalışıyor, yani kopya alındığında arka plandaki tampon paylaşılıyor ve ancak taraflardan biri yazmaya kalkışınca gerçekten çoğaltılıyor.
Günlük hayatta en çok işime yarayan kazanç ise şu: struct’lar için Equatable, Hashable ve Codable uyumlarını çoğu durumda derleyici sentezliyor. Java’da elle yazdığım equals/hashCode çiftlerinin ve JSON eşleme kodunun karşılığı burada tek satır oluyor. Firestore belgelerini modele çevirirken bundan çok faydalandım.
class’ı ne zaman seçiyorum? Nesnenin kimliği önemliyse, yani aynı örneğin birden çok yerden paylaşılması gerekiyorsa; UIKit sınıflarından türetiyorsam; ya da deinit’e ihtiyacım varsa. Geri kalan her şey struct.
Protokoller, arayüzlerin biraz ötesi
Protokolü ilk gördüğümde “bu interface” dedim ve çoğu yerde yanılmadım. Fark uzantılarla başlıyor: protokole varsayılan davranış yazabiliyorsun. Java 8’in default metotları benzer işi görüyor, ama Swift’te bu uzantılar sınıflara olduğu kadar struct ve enum’lara da uygulanıyor; üstelik kendi yazmadığın bir tipi sonradan protokole uyumlu hâle getirebiliyorsun.
Beni asıl yanıltan gönderim davranışı oldu:
protocol Fiyatli { var fiyat: Decimal { get } }
extension Fiyatli {
func etiket() -> String { "\(fiyat) TL" }
}
struct Urun: Fiyatli {
var fiyat: Decimal
func etiket() -> String { "kampanya: \(fiyat) TL" }
}
let u: Fiyatli = Urun(fiyat: 100)
u.etiket() // "100 TL" — Urun'un metodu değil, uzantıdaki metot
Metot protokolün kendisinde bildirilmediği için çağrı, değişkenin statik tipine göre derleme zamanında çözülüyor. Java’da sanal gönderim varsayılan olduğundan bu davranışı bir hata sanıp saatlerimi harcadım. Kural basit: uyumlu tiplerin ezmesini istiyorsan metodu uzantıya ek olarak protokolde de bildir.
İkinci konu ilişkili tipler. İçinde associatedtype geçen bir protokolü doğrudan değişken tipi gibi kullanamıyorsun; burada some ve any anahtar kelimeleri devreye giriyor. Java’daki jenerik arayüz alışkanlığıyla yaklaşınca derleyicinin neden itiraz ettiğini anlamak zaman aldı.
ARC bir çöp toplayıcı değil
JVM’de erişilemez hâle gelen nesneleri, aralarında döngüsel referans olsa bile toplayan bir çöp toplayıcı var. Swift’te ARC var: derleyici, referans sayacını artıran ve azaltan çağrıları kodun içine yerleştiriyor, sayaç sıfıra inince nesne hemen serbest bırakılıyor. Deterministik olması iyi, ama döngüsel referansları kimse toplamıyor.
İlk retain cycle’ımı bir Firestore dinleyicisinde yaşadım. Ekranı kapattığımda deinit hiç çalışmıyor, geri dönüp tekrar girdiğimde aynı veri için iki dinleyici birden tetikleniyordu. Sebep, bloğun self’i güçlü yakalaması ve nesnenin dinleyiciyi tutmasıydı:
final class SiparisDinleyici {
private var kayit: ListenerRegistration?
func basla() {
kayit = db.collection("siparisler")
.addSnapshotListener { [weak self] anlik, hata in
guard let self else { return }
self.guncelle(anlik)
}
}
deinit { kayit?.remove() }
}
Buradan iki alışkanlık kaldı: delegate özelliklerini weak var tanımlamak ve dinleyici kayıtlarını ekran kapanırken iptal etmek. Xcode’un Memory Graph Debugger’ı hangi nesnenin kimi tuttuğunu gösteriyor, ama en hızlı teşhis yöntemi hâlâ sınıfın deinit’ine bir print koyup ekranı kapatmak.
weak ile unowned farkı da şu: weak isteğe bağlıdır, hedef yok olunca nil olur. unowned nil olmaz; hedef yok olduktan sonra erişirsen uygulama çöker. Ömrün gerçekten iç içe olduğundan emin olmadığım her yerde weak kullanıyorum.
Closure yakalama listeleri
Java’da lambda içinde kullandığın yerel değişkenin “effectively final” olması gerekir ve değeriyle yakalanır. Swift’te closure, değişkenin kendisini yakalar: dışarıdaki bir var’ı closure içinden değiştirebilirsin, değişiklik dışarıda da görünür. Yakalama listesi ise closure oluşturulduğu anda değerlendirilip kopya alır:
var sayac = 0
let a = { [sayac] in print(sayac) } // oluşturulurken kopyalandı
let b = { print(sayac) } // değişkenin kendisi yakalandı
sayac = 5
a() // 0
b() // 5
Bir de @escaping var: closure fonksiyon döndükten sonra saklanıp çağrılacaksa imzada bunu belirtmen gerekiyor. Firebase’in tamamlanma blokları böyle olduğu için self’i güçlü yakalama riski tam da orada doğuyor. Ağ ve veritabanı çağrılarında refleksim artık doğrudan [weak self] yazmak.
UIKit ile SwiftUI arasında kalmak
Swift’e geçtiğimde arayüz tarafında UIKit öğrendim: görünüm yaşam döngüsü, Auto Layout, delegate ve dataSource kalıpları. Android’de Activity ve Fragment yaşam döngüsüyle uğraşmış olmak burada işimi kolaylaştırdı, çünkü ikisi de aynı soruyu soruyor: bu iş hangi anda yapılmalı?
SwiftUI’ya geçerken asıl zorlanma noktası söz dizimi değil, düşünme biçimiydi. UIKit’te ekranı sen güncellersin; SwiftUI’da durumu güncellersin, ekran kendini yeniden çizer. En çok takıldığım yer sahiplik oldu: bir gözlemlenebilir nesneyi ekranın kendisi oluşturuyorsa @StateObject, dışarıdan alıyorsa @ObservedObject kullanılır. Bunu karıştırdığımda nesne her yeniden çizimde baştan oluşuyor ve durum sessizce sıfırlanıyordu.
Geçiş dönemi de tek yönlü olmadı. Görüntülü görüşme ekranını yaparken kullandığım SDK UIKit tabanlıydı; SwiftUI ekranına almak için UIViewControllerRepresentable ile sarmalamak gerekti. Tersi de mümkün: mevcut UIKit projesinin içine UIHostingController ile SwiftUI ekranı gömülebiliyor. Uygulamalarımda uzun süre ikisi bir arada çalıştı.
Bir de en az destekleyeceğin iOS sürümü meselesi var. SwiftUI API’lerinin önemli bir kısmı belirli sürümlerle geldiği için, eski cihazları da desteklemek isteyince bazı ekranları UIKit’te bırakmak en pratik çözüm oldu. UIKit bilgisi de boşa gitmiyor; SwiftUI beklenmedik davrandığında altta ne olduğunu anlamak için hâlâ oraya bakıyorum.
Java’dan gelecek olana kısa liste
- Optional’ı geçiştirme.
!yerineguard letyaz; ünlem sadece gerçekten devam edilemeyecek durumlar için. - Varsayılanın struct olduğunu kabul et. class’ı kimlik veya
deinitgerektiğinde seç. letilefinal’ı eşitleme. Sınıflarda sabitlenen referanstır, içerik değil.- Protokol uzantısındaki metot her zaman ezilmez. Ezilmesini istiyorsan protokolde de bildir.
- Bellek senin sorumluluğunda. delegate’ler weak, dinleyiciler kapanırken iptal.
- Kaçan closure’larda self’i güçlü yakalama.
[weak self]alışkanlık olsun.
Geriye dönüp baktığımda Swift’i zorlaştıran şey dilin kendisi değil, Java’dan getirdiğim varsayımlardı. Derleyicinin itiraz ettiği her yerde “bu dil neden böyle” diye sormak yerine ne korumaya çalıştığını anlamak, öğrenme süremi kısaltan tek şey oldu. 2026’da Flutter ve Dart’a yönelmiş olsam da, platform tarafına inmek gerektiğinde bu bilgilerin hiçbiri boşa gitmedi.
- Swift
- iOS
- Java
- SwiftUI
- Yazılım
Demir Taşdemir
Mobil Uygulama & Web Geliştirici
2018'den beri yazılım geliştiriyorum. App Store ve Google Play'de 11 uygulama yayınladım; şu an 6 mobil uygulama, 1 e-ticaret platformu ve 1 masaüstü oyun üzerinde çalışıyorum.
Mobil tarafta bir fikriniz mi var?
Swift, Java, Kotlin ve Flutter ile yayınladığım uygulamalara göz atabilir, projeniz için doğrudan bana yazabilirsiniz.