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

Swift Struct ve Class Farkı: Değer ve Referans Tipleri

Ahmet Balaman

5 dk okuma

SwiftStructClassValue TypeReference TypeiOS
Swift Struct ve Class Farkı: Değer ve Referans Tipleri

Swift'te struct ve class dışarıdan neredeyse aynı görünür: ikisinin de özellikleri, metotları, initializer'ları olur. Asıl fark, bir değişkeni başka bir değişkene atadığınızda ortaya çıkar. Struct değer tipidir, atandığında kopyalanır. Class referans tipidir, atandığında aynı nesneye ikinci bir işaretçi oluşur. Bu tek cümleyi gerçekten anlamak, hem sınav sorularının hem de gerçek projelerdeki "bu değer neden değişti?" hatalarının büyük kısmını çözer.

Aşağıdaki örneklerin hepsini bir Playground'a ya da main.swift dosyasına yapıştırıp çalıştırabilirsiniz.

Değer Tipi: Struct Kopyalanır

struct Point {
    var x: Int
    var y: Int
}

var a = Point(x: 1, y: 2)
var b = a          // kopya
b.x = 10

print(a.x)  // 1
print(b.x)  // 10

b = a satırında a'nın bağımsız bir kopyası oluşur. b'yi değiştirmek a'ya dokunmaz. Int, String, Array, Dictionary ve Bool da Swift'te struct olarak tanımlıdır, yani hepsi aynı şekilde davranır. Bir fonksiyona dizi verdiğinizde fonksiyonun sizin dizinizi bozamayacağından emin olmanızın sebebi budur.

Dikkat ederseniz Point için initializer yazmadık. Struct'lar memberwise initializer'ı bedavaya alır.

Referans Tipi: Class Paylaşılır

final class Player {
    var name: String
    var score = 0

    init(name: String) {
        self.name = name
    }
}

let p1 = Player(name: "Ada")
let p2 = p1        // aynı nesneye ikinci referans
p2.score = 50

print(p1.score)    // 50
print(p1 === p2)   // true

Burada kopya yok. p1 ve p2 bellekteki tek bir Player nesnesini gösteriyor; birinden yapılan değişiklik diğerinden de görünüyor. Class'ta initializer'ı kendimiz yazmak zorunda kaldık; varsayılan değeri olmayan bir özellik varsa derleyici bunu ister.

Değiştirilebilirlik: mutating ve let

Struct'ın bir metodu kendi özelliğini değiştirecekse başına mutating yazılır:

struct Counter {
    private(set) var value = 0

    mutating func increment() {
        value += 1
    }
}

var counter = Counter()
counter.increment()
print(counter.value)   // 1

let fixedCounter = Counter()
// fixedCounter.increment()
// Hata: cannot use mutating member on immutable value: 'fixedCounter' is a 'let' constant

let ile tanımlanan bir struct tamamen donar: hiçbir özelliği değişmez. Çünkü struct'ın değeri, özelliklerinin toplamıdır; bir özelliği değiştirmek değerin kendisini değiştirmektir.

Class'ta let farklı anlam taşır:

let player = Player(name: "Linus")
player.score = 10          // serbest: nesnenin içi değişiyor
// player = Player(name: "Grace")
// Hata: cannot assign to value: 'player' is a 'let' constant

let burada yalnızca referansı sabitler: player başka bir nesneyi gösteremez, ama gösterdiği nesnenin içi değişebilir. Class metotlarında mutating diye bir şey de yoktur. Bu, değişmezlik garantisi istiyorsanız struct'ın neden daha güvenli olduğunu gösterir.

Kimlik ve Eşitlik: === ile ==

İki farklı soru var: "bunlar aynı nesne mi?" ve "bunların içeriği eşit mi?"

struct Money: Equatable {
    var amount: Int
    var currency: String
}

let price1 = Money(amount: 100, currency: "TRY")
let price2 = Money(amount: 100, currency: "TRY")
print(price1 == price2)    // true

let first = Player(name: "Ada")
let second = Player(name: "Ada")
let alias = first

print(first === second)    // false
print(first === alias)     // true

=== yalnızca class örnekleri için vardır ve kimliği karşılaştırır: aynı bellek nesnesi mi? Struct'ların kimliği yoktur, sadece değeri vardır; 100 TL her yerde 100 TL'dir. == kullanmak için tipin Equatable olması gerekir. Struct'ta bütün özellikler Equatable ise derleyici =='i sizin yerinize üretir; class'ta kendiniz yazarsınız.

Kalıtım Sadece Class'ta

class Vehicle {
    var wheels: Int

    init(wheels: Int) {
        self.wheels = wheels
    }

    func describe() -> String {
        "\(wheels) tekerlekli araç"
    }
}

class Car: Vehicle {
    init() {
        super.init(wheels: 4)
    }

    override func describe() -> String {
        "Otomobil: " + super.describe()
    }
}

let vehicle: Vehicle = Car()
print(vehicle.describe())  // Otomobil: 4 tekerlekli araç

Struct başka bir struct'tan türeyemez; denerseniz inheritance from non-protocol type hatası alırsınız. Ortak davranış struct'larda protocol ile tanımlanır:

protocol Describable {
    func describe() -> String
}

struct Bicycle: Describable {
    func describe() -> String { "2 tekerlekli bisiklet" }
}

Swift'te genel eğilim kalıtım yerine protocol kullanmaktır. C# ya da Java'dan geliyorsanız bu alışkanlık değişikliği biraz zaman alır.

deinit: Nesnenin Ömrü

Class örnekleri referans sayımıyla (ARC) yaşar. Bir nesneyi gösteren son güçlü referans da gidince nesne bellekten silinir ve deinit çalışır:

final class FileSession {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) açıldı")
    }

    deinit {
        print("\(name) kapatıldı")
    }
}

var session: FileSession? = FileSession(name: "log.txt")  // log.txt açıldı
var sameSession = session
session = nil          // henüz bir şey yazmaz, sameSession hâlâ tutuyor
sameSession = nil      // log.txt kapatıldı

Struct'larda deinit yoktur, çünkü paylaşılan bir ömürleri yoktur. İki class nesnesi birbirini güçlü referansla tutarsa ikisi de hiç silinmez; buna retain cycle denir ve en sık closure'larda karşınıza çıkar. Ayrıntısı closure yazısındaki [weak self] bölümünde.

SwiftUI View'ları Neden Struct?

SwiftUI'da her ekran parçası struct SomeView: View şeklinde yazılır. Bunun sebebi, view'ın ekrandaki kalıcı bir nesne değil, ekranın o anki hâlinin hafif bir tarifi olmasıdır. SwiftUI veri değiştikçe bu tarifleri yeniden üretir; bu yüzden ucuz oluşturulabilen, gizlice paylaşılan durumu olmayan bir tipe ihtiyaç vardır.

Struct kendi özelliğini body içinden değiştiremeyeceği için, değişen veri @State gibi sarmalayıcılarla SwiftUI'ın kendi sakladığı bir yere taşınır. Bunun pratikte nasıl göründüğünü SwiftUI ile ilk uygulama yazısında görebilirsiniz. Birden fazla ekranın paylaştığı veri içinse class kullanılır; o tarafı @State, @Binding ve @Observable yazısı anlatıyor.

Gerçekçi Senaryo: Sepet

Tipik bir uygulamada ikisi birlikte kullanılır: veri struct, o veriyi paylaşan yönetici class.

struct CartItem: Identifiable, Equatable {
    let id: Int
    var name: String
    var quantity: Int
    var unitPrice: Double

    var total: Double { Double(quantity) * unitPrice }
}

final class CartStore {
    private(set) var items: [CartItem] = []

    var total: Double {
        items.reduce(0) { $0 + $1.total }
    }

    func add(_ item: CartItem) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index].quantity += item.quantity
        } else {
            items.append(item)
        }
    }
}

let store = CartStore()
let checkoutStore = store            // iki ekran aynı sepeti görüyor

store.add(CartItem(id: 1, name: "Kalem", quantity: 2, unitPrice: 15))
checkoutStore.add(CartItem(id: 1, name: "Kalem", quantity: 1, unitPrice: 15))

print(store.items[0].quantity)   // 3
print(checkoutStore.total)       // 45.0

var snapshot = store.items       // dizinin kopyası
snapshot[0].quantity = 99
print(store.items[0].quantity)   // 3, sepet etkilenmedi

Ürün listesi ekranı da ödeme ekranı da aynı sepeti görmeli; burada paylaşım istenen şeydir, o yüzden CartStore class. CartItem ise bir kimliği olmayan düz veridir; struct olduğu için snapshot üzerinde yapılan deneme sepeti bozmaz. Değişikliklerin tek kapısı add metodudur.

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

Varsayılan tercih struct olsun. Şu durumlarda class'a geçin:

  • Aynı örneği birden fazla yerin paylaşması ve değişikliği herkesin görmesi gerekiyorsa (sepet, oturum, ayarlar yöneticisi).
  • Nesnenin bir kimliği ve ömrü varsa: açık bir dosya, ağ bağlantısı, zamanlayıcı. Temizlik için deinit gerekir.
  • Kalıtıma ihtiyaç varsa ya da UIViewController gibi class bekleyen bir Apple API'si ile çalışıyorsanız.

Şu durumlarda struct'ta kalın: modeller (kullanıcı, ürün, koordinat), API'den gelen Codable veriler, SwiftUI view'ları, birden fazla thread'e geçecek veriler. Değer tipleri kopyalandığı için async/await ile eşzamanlı kodda paylaşılan durum kaynaklı hatalara çok daha az açıktır.

Soru Struct Class
Atamada ne olur? Kopyalanır Referans paylaşılır
let örnek Tamamen değişmez Sadece referans sabit
Kalıtım Yok (protocol var) Var
=== ve deinit Yok Var
Hazır initializer Memberwise Yalnızca tüm özellikler varsayılan değerliyse

Sık Yapılan Hatalar

1. Kopyayı değiştirip orijinalin değişmesini beklemek

struct Todo {
    var title: String
    var isDone = false
}

var todos = [Todo(title: "Ödev")]
var firstTodo = todos[0]
firstTodo.isDone = true
print(todos[0].isDone)   // false

Belirti: hata yok, ama liste güncellenmiyor. todos[0] bir kopya döndürdü. Çözüm: yerinde değiştirmek, yani todos[0].isDone = true.

2. mutating yazmayı unutmak

Belirti: cannot assign to property: 'self' is immutable ya da left side of mutating operator isn't mutable: 'self' is immutable. Çözüm: metodu mutating func yapmak. Aynı hatayı SwiftUI view'ında alıyorsanız çözüm mutating değil @State'tir.

3. Class'ı farkında olmadan paylaşmak

Belirti: bir ekranda yaptığınız düzenleme, "İptal"e bassanız bile diğer ekranda görünüyor. Düzenleme ekranına class örneğinin kendisini verdiniz, kopyasını değil. Çözüm: modeli struct yapmak; düzenleme ekranı kopya üzerinde çalışır, "Kaydet" denince geri yazılır.

4. Class'a initializer yazmamak

Belirti: class 'Player' has no initializers. Struct'taki memberwise initializer class'ta yoktur. Ya her özelliğe varsayılan değer verin ya da init yazın.

Sınavda ve Mülakatta Sorulanlar

"Bu kod ne yazdırır?" soruları neredeyse her zaman atamanın kopya mı referans mı olduğunu ölçer. Önce tipe bakın: struct ise iki değişken bağımsızdır, class ise aynıdır.

"Array değer tipi mi?" Evet. İçindeki elemanlar class ise dizi kopyalanır ama elemanlar aynı nesneleri göstermeye devam eder. Yukarıdaki Todo örneğini class ile yazsaydık todos[0].isDone true çıkardı.

"Büyük bir diziyi kopyalamak yavaş olmaz mı?" Standart kütüphane koleksiyonları copy-on-write kullanır: gerçek kopya ancak kopyalardan biri değiştirildiğinde yapılır. Kendi yazdığınız struct'lar için bu otomatik değildir, ama içindeki diziler yine aynı şekilde davranır.

"Struct stack'te, class heap'te mi durur?" Yaygın bir basitleştirme; derleyici duruma göre farklı karar verebilir. Cevabınızı bellek konumuna değil, kopyalama ve paylaşma davranışına dayandırın. Bu tür soruları çalışmak için sınav destek sayfasına göz atabilirsiniz.

Sık Sorulan Sorular

Swift'te struct mı class mı daha hızlı?

Genel bir cevap yok; struct'lar referans sayımı yükü taşımadığı için çoğu küçük modelde avantajlıdır, ama çok büyük struct'ları sürekli kopyalamak da pahalı olabilir. Seçimi hıza göre değil, paylaşım gerekip gerekmediğine göre yapın.

Struct içinde class özelliği olursa ne olur?

Struct kopyalanır ama içindeki class özelliği aynı nesneyi göstermeye devam eder. Yani kopyalar o nesne üzerinden birbirini etkileyebilir; değer semantiği istiyorsanız iç tipleri de struct tutun.

let ile tanımlanan class örneğinin özelliği neden değişebiliyor?

Çünkü let referansı sabitler, nesnenin içeriğini değil. Özelliğin değişmesini istemiyorsanız o özelliği class içinde let olarak tanımlayın.

Struct'lar protocol uygulayabilir mi?

Evet. Struct'lar kalıtım alamaz ama istediği kadar protocol'e uyabilir; Identifiable, Equatable, Codable en sık kullanılanlardır.

Yorumlar