İçeriğe geç
academia.sh

Ders 11 / 21

Yapılar ve Sınıflar

Yapıların atamada kopyalanmasının, sınıfların paylaşılmasının, varsayılan değerin ne zaman kurulduğunun ve kopyanın ilk değişiklikte oluşmasının ölçümü.

Yayın Güncelleme

Bu derste

  • Yapı atamasının bağımsız bir kopya, sınıf atamasının paylaşılan bir nesne verdiğini ölçer
  • Varsayılan değerin yalnızca verilmediğinde ve her örnekte ayrı değerlendirildiğini gösterir
  • Yazarken kopyalamada kopyanın atamada değil ilk değişiklikte oluştuğunu ölçer
  • Tek sahipli ve sınıf içinde paylaşılan değerde kopyanın hiç oluşmadığını gösterir
İçindekiler

Önceki ders kutuyu açmanın yollarını ölçtü ve her yolun ifadeyi ne zaman değerlendirdiğini sıraladı. Açılan değer bir Int ya da bir String olabilir, ama programın asıl verisi çoğu zaman birkaç alanı bir arada tutan bir tiptir: bir nokta, bir sipariş, bir belge. Swift’te böyle tiplerin iki temel biçimi vardır (numaralandırmalar ve aktörler ayrı derslerin konusu). Yapı (structure) bir değer tipidir: atandığında içeriği kopyalanır. Sınıf (class) bir referans tipidir: atandığında aynı nesneye ikinci bir ad verilir.

Bu fark çoğu kitapta “kim neyi görüyor” sorusuyla anlatılır: bir adın değişikliğini öbür ad görüyor mu? Bu ders başka bir soruyu sorar: kopya ne zaman oluşuyor? Atamanın kendisi bir kopya mı üretir, yoksa kopya yalnızca ilk değişiklikte mi ortaya çıkar? Kursun ölçü ekseni bu soruyu dört ana ayırır: hemen, ilk kullanımda, her kullanımda, hiç. Probe sınıfı aşağıda olduğu gibi verilir.

Çekirdek

Her ders aynı çekirdeği kullanır. Probe bir ifadenin kaç kez değerlendirildiğini (eval) ve kaç kez kullanıldığını (use) sayar; moment özelliği bu iki sayıdan anı çıkarır. Açılış dersindeki bağlayıcı ölçümde sekiz ifadenin dağılımı 2/1/3/2 idi: ikisi hemen, biri ilk kullanımda, üçü her kullanımda, ikisi hiç. Bu derste yalnız kendi kesitini ölçeriz.

// 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)")
    }
}

Aşağıdaki ilk ölçümler üç tip kullanır. Point iki tamsayı tutan bir yapıdır, Counter tek bir sayaç tutan bir sınıftır. Order varsayılan değerli bir alana sahip bir yapıdır; varsayılan değer bir işlev çağrısıdır ve işlev çalıştığında bir satır yazdırır. Bu bloğun çıktısı yoktur, çünkü yalnızca tipleri tanımlar.

// Types.swift — bir yapi, bir sinif ve varsayilan degerli bir yapi
struct Point {
    var x: Int
    var y: Int
}

final class Counter {
    var value = 0
}

func makeNote() -> String {
    print("varsayilan deger kuruldu")
    return "yok"
}

struct Order {
    var id: Int
    var note = makeNote()
}

Atama: Kopya mı, Paylaşım mı

Aynı iki satırı bir yapıyla ve bir sınıfla yazalım. Yapıda var second = first ardından second.x = 10 yazıldığında first değişmez. Sınıfta let right = left ardından right.value = 5 yazıldığında iki ad aynı nesneye bağlıdır ve her ikisi de 5 görür.

// main.swift — yapi atamada kopyalanir, sinif paylasilir
let first = Point(x: 1, y: 2)
var second = first
second.x = 10
print(first.x, second.x)

let left = Counter()
let right = left
right.value = 5
print(left.value, right.value)
1 10
5 5

Birinci satır yapıyı gösterir: first.x 1 kaldı, second.x 10 oldu. Atama bağımsız bir kopya verdi. İkinci satır sınıfı gösterir: left.value ve right.value ikisi de 5’tir, çünkü tek bir Counter nesnesi vardır ve iki ad ona bağlıdır. Dikkat edin ki right bir let sabitidir ve yine de nesnenin içindeki value değişebildi: let adın başka bir nesneye bağlanmasını yasaklar, bağlı nesnenin içeriğini yasaklamaz. Yapıda ise let bütün değerin değişmezliği anlamına gelir: değişmez bir yapının alanı, alan var olarak tanımlanmış olsa bile atanamaz.

Çıktı iki tipi birbirinden ayırır ama kopyanın ne zaman oluştuğunu söylemez. Yapı için “atamada kopyalanır” cümlesi ders kitaplarında bir model olarak vardır; ölçtüğümüz şey ise gözlenebilir davranıştır: değişiklikten sonra iki değer bağımsızdır. Kopyanın fiziksel olarak atamada mı yoksa sonradan mı oluştuğu aşağıda ölçülecek.

TS9. Bu ölçüm iki tamsayı alanlı bir yapı ve tek alanlı bir sınıf için. Alanların başka bir sınıfa referans tuttuğu yapılar ve yapı içindeki sınıf alanlarının kopyalanması bu ölçümde yok.

İşleve Geçirme

Atama değer ile referans ayrımının yalnız bir yüzüdür; aynı ayrım bir işleve argüman verirken de yaşanır. Bir işlev yapıyı parametre olarak aldığında kendi kopyasıyla çalışır: içeride yapılan değişiklik, çağıran tarafın değerine ulaşmaz. Sınıf parametresinde ise işlev ile çağıran aynı nesneye bakar. Aşağıdaki ölçümde moved yapının bir kopyasını değiştirip döndürür, bump ise sayacı yerinde değiştirir.

// main.swift — isleve gecirilen yapi kopyalanir, sinif paylasilir
func moved(_ point: Point) -> Point {
    var copy = point
    copy.x += 100
    return copy
}

func bump(_ counter: Counter) {
    counter.value += 100
}

let origin = Point(x: 1, y: 2)
let shifted = moved(origin)
print(origin.x, shifted.x)

let ticker = Counter()
bump(ticker)
print(ticker.value)
1 101
100

Birinci satırda origin.x 1 olarak kaldı, shifted.x ise 101 oldu: moved içindeki copy ayrı bir değerdi ve değişiklik yalnızca ona yazıldı. İkinci satırda ticker.value 100 oldu, çünkü bump aynı Counter nesnesini değiştirdi ve işlevin döndürdüğü bir şey yok. Bu yüzden parametresi yapı olan bir işlevin imzası, yapı yalnız değer tutuyorsa ve parametre inout değilse, okuyana “bu işlev çağırdığım değeri değiştirmez” der. Parametresi sınıf olan bir işlevin imzası böyle bir güvence vermez; işlevin gövdesine bakmak gerekir.

Bu ayrımın pratik sonucu, hata ayıklamada görünür. Bir yapının değeri beklenmedik biçimde değiştiyse, değişiklik o değerin kendi kapsamında yapılmıştır; bir sınıf nesnesi beklenmedik biçimde değiştiyse, nesneye ulaşabilen her yer şüphelidir. İşlev imzaları arasında değer tipi kullanmak şüpheli alanı daraltır.

TS10. Bu ölçüm bir yapı ve bir sınıf parametresi için. inout parametre ile geçirme ve kapanışın yakaladığı yapı bu ölçümde yok.

Varsayılan Değer: Her Örnekte Bir Kez

Order yapısının note alanı makeNote() işlevinin sonucuyla başlar. Bu varsayılan değer bir kez kurulup bütün örnekler arasında paylaşılmaz; her Order örneği kendi varsayılan değerini oluşturur. Üstelik değer, çağıran taraf alanı kendisi vermişse hiç hesaplanmaz. Üç siparişle ölçelim: ikisi varsayılan notla, biri açık bir notla.

// main.swift — varsayilan deger her ornekte, ama yalniz verilmediyse degerlendirilir
let o1 = Order(id: 1)
let o2 = Order(id: 2)
let o3 = Order(id: 3, note: "acele")
print(o1.note, o2.note, o3.note)
varsayilan deger kuruldu
varsayilan deger kuruldu
yok yok acele

makeNote() iki kez çalıştı, üç siparişin ikisi için. Notu açıkça veren üçüncü sipariş için işlev hiç çağrılmadı. Çalıştığı iki yer de yazdırma satırından önce görünür; yani varsayılan değer, örnek kurulurken, çağıran satırda hesaplandı.

Bir sonuç çıkar: varsayılan değer olarak yazılan her ifade örnek sayısı kadar değerlendirilir, verilen değer sayısı kadar eksik. Değerlendirmesi pahalı bir ifade varsayılan değer olarak yazılırsa, bu maliyet her örnekte ödenir. Özellikler dersinde lazy ile bu maliyetin ertelenmesi ölçülecek.

TS11. Bu ölçüm üç örnekle ve bir varsayılan değerli alanla yapıldı. Birden çok varsayılan değerli alanın değerlendirme sırası ve sınıflarda aynı durum ayrıca ölçülmedi.

Yazarken Kopyalama

Büyük bir değer tipi atandığında gerçekten tamamını kopyalamak israf olurdu. Standart kitaplığın bazı tipleri bu yüzden yazarken kopyalama (copy-on-write) denen bir teknik kullanır: atama yalnızca bir depoya ikinci bir referans ekler; kopya ancak ortak depoyu değiştirmek gerektiğinde, ilk değişiklikte oluşur. Aynı tekniği kendi tipimizde elle kurabiliriz. Document yapısı metni bir Storage sınıfında tutar. append önce depoya tek başına mı sahip olduğumuzu isKnownUniquelyReferenced ile sorar; değilse depoyu kopyalar ve kopyalama anını onCopy kapanışıyla bildirir.

// Document.swift — sinif kutulu, yazarken kopyalayan yapi
final class Storage {
    var text: String

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

struct Document {
    private var storage: Storage
    private let onCopy: () -> Void

    init(text: String, onCopy: @escaping () -> Void) {
        storage = Storage(text: text)
        self.onCopy = onCopy
    }

    var text: String {
        storage.text
    }

    mutating func append(_ more: String) {
        if !isKnownUniquelyReferenced(&storage) {
            onCopy()
            storage = Storage(text: storage.text)
        }
        storage.text += more
    }
}

final class Holder {
    var document: Document

    init(_ document: Document) {
        self.document = document
    }
}

Koda yakından bakalım. storage alanı private olduğu için dışarıdan görünmez; dışarıya yalnızca salt okunur text özelliği açılır. Değişiklik yalnız mutating olarak işaretli append içinden geçer ve kopyalama kararı da yalnızca orada verilir. Bu daraltma önemlidir: kopyalama kuralı tek bir yerde durduğu için, tipin başka hiçbir yöntemi depoyu paylaşılan halde değiştiremez. onCopy kapanışı ise ölçüm içindir; üretim kodunda böyle bir bildirim bulunmaz, ama burada kopyanın tam olarak hangi çağrıda oluştuğunu saymamızı sağlar. Holder sınıfı ise son ölçümde kullanılacak: bir Document değerini bir sınıf nesnesinin alanı yapar.

Bu bloğun çıktısı yoktur; yalnızca Document ve onu bir sınıfa saran Holder tanımlıdır. Şimdi duplicate = original ataması yapılıp ikisi de değiştirilince kopyanın tam olarak ne zaman oluştuğunu ölçelim. onCopy kapanışı cow.eval ile sarılıdır; iki kez append çağrılır.

// main.swift — kopya atamada degil, ilk degisiklikte olusur
let cow = Probe("yazarken-kopyalama")
let original = Document(text: "ab", onCopy: { _ = cow.eval(()) })
var duplicate = original

_ = cow.use { duplicate.append("c") }
_ = cow.use { duplicate.append("d") }
cow.report()
print(original.text, duplicate.text)
yazarken-kopyalama: 1 degerlendirme, 2 kullanim -> ilk-kullanimda
ab abcd

Atamanın kendisi kopya üretmedi: değerlendirme sayısı bir ve an ilk kullanımda. İlk append depoyu başka bir değerle paylaşıyordu, bu yüzden kopya o anda oluştu. İkinci append kopyalanan depoya tek başına sahipti, kopya gerekmedi. Son satır sonucu doğrular: original hâlâ ab, duplicate ise abcd. Ortada iki değer var ama yalnızca bir kopyalama yaşandı ve o da atamada değil, ilk değişiklikte.

Sayıların anlamını da yazalım. Bir değerlendirme, kopyanın bir kez oluştuğunu söyler; iki kullanım, iki append çağrısı yapıldığını söyler. Değerlendirme sayısı kullanım sayısına eşit olsaydı her değişiklik kopya doğururdu ve an her kullanımda olurdu. Sıfır olsaydı hiçbir değişiklik kopya gerektirmezdi ve an hiç olurdu. Ölçülen bir, ikinin altındadır ve ilk kullanımda oluşmuştur; bu üç sayı birlikte anı belirler.

Bu ölçüm başta sorduğumuz soruyu yanıtlar. Depoyu paylaşan Document gibi bir değer tipinde kopya atamadan sonra, ilk yazma sırasında oluşur. Okuyan taraf için bir fark yoktur; kopyanın bedeli, ilk yazan tarafa düşer. Bu yüzden değer tipi ile referans tipi arasındaki seçim yalnız anlamsal bir tercih olarak değil, bedelin ne zaman ödeneceği olarak da okunabilir.

TS12. Bu ölçüm yalnızca elle kurduğumuz Document tipi için; standart kitaplığın dizi, sözlük ve dize tiplerinin kendi yazarken kopyalamaları bu derste ölçülmedi.

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

Yazarken kopyalama için kurduğumuz kural şu: kopya ilk değişiklikte oluşur. Kuralın bir koşulu vardır, depo başka bir sahiple paylaşılıyor olmalı. İki durumda bu koşul oluşmaz. Birincisinde solo belgesine kimse ortak olmamıştır; ikincisinde belge bir sınıfın içindedir ve iki ad aynı sınıf nesnesine bağlıdır, yani depoyu paylaşan ikinci bir Document değeri yoktur. Her ikisinde de iki kez append çağrılır.

// main.swift — tek sahipli ya da sinif icinde paylasilan belge kopyalanmaz
let unique = Probe("tek-sahipli")
var solo = Document(text: "ab", onCopy: { _ = unique.eval(()) })
_ = unique.use { solo.append("c") }
_ = unique.use { solo.append("d") }

let shared = Probe("sinif-paylasimi")
let holder = Holder(Document(text: "ab", onCopy: { _ = shared.eval(()) }))
let alias = holder
_ = shared.use { holder.document.append("c") }
_ = shared.use { alias.document.append("d") }

unique.report()
shared.report()
print(solo.text, holder.document.text, alias.document.text)
tek-sahipli: 0 degerlendirme, 2 kullanim -> hic
sinif-paylasimi: 0 degerlendirme, 2 kullanim -> hic
abcd abcd abcd

İki durumda da onCopy hiç çağrılmadı: an hiç. Tek sahipli belge ilk değişiklikte bile kopyalanmadı, çünkü paylaşılacak bir ikinci sahip yoktu. Sınıf içindeki belge de kopyalanmadı: holder ve alias aynı Holder nesnesine bağlı olduğundan, bir ad üzerinden yapılan ekleme ötekinde de görünür ve Document değerinin ikinci bir kopyası hiç oluşmaz. Son satır bu paylaşımı doğrular: üç ad da abcd yazar.

Bu sınır dersin kuralını daraltır. “Değer tipi atamada kopyalanır” cümlesi ve onu tamamlayan “kopya ilk değişiklikte oluşur” cümlesi, yalnızca depoyu paylaşan iki ayrı değer varsa geçerlidir. Değer tipi bir sınıfın alanıysa sınıfın referans anlamı baskın çıkar: kopya oluşmaz, çünkü sınıf nesnesi zaten tektir.

TS13. Bu ölçüm tek bir sınıf nesnesi ve iki ad için. Sınıf nesnesinin kendisi Document alanıyla birlikte başka bir değere atandığında ya da iki farklı Holder nesnesinin aynı belgeyi taşıdığı durum ölçülmedi.

Özet

  • Yapı atamada bağımsız bir kopya verir; sınıf atamasında iki ad aynı nesneye bağlanır.
  • let ile tanımlanan sınıf adı nesneyi değişmez yapmaz; let ile tanımlanan yapı bütün değeri değişmez yapar.
  • Varsayılan değer her örnekte bir kez değerlendirilir ve çağıran alanı verdiyse hiç değerlendirilmez.
  • Yazarken kopyalamada kopya atamada değil ilk değişiklikte, depo paylaşılıyorsa oluşur.
  • Tek sahipli ya da sınıf içinde paylaşılan değerde kopya hiç oluşmaz.
  • Bu dersin anı: ilk kullanımda. Yazarken kopyalanan belgede kopya ilk değişiklikte oluşur.

Sonraki Adım

Bu derste bir tipin değer mi referans mı olduğu ve kopyanın ne zaman oluştuğu ölçüldü. Bazı tiplerin bir alan listesi değil, birkaç ayrı durumdan birinde olması gerekir: bir ölçüm ya derece cinsindendir ya da bir etikettir. Bir sonraki ders bu durumları ve her durumla birlikte taşınan değerleri anlatan numaralandırmaları ölçer.

İ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