Ders 20 / 21
Sonuç Kurucular
Kendi sonuç kurucumuzu yazar; seçilmeyen if ve else dalındaki ifadelerin hiç değerlendirilmediğini, for döngüsünün her elemanı değerlendirdiğini ve seçimin derlemede değil koşumda yapıldığını ölçer.
Yayın Güncelleme
Bu derste
- Bir dizgi listesi kuran kendi sonuç kurucusunu buildBlock ve buildExpression ile yazar
- Seçilmeyen if ve else dalındaki ifadelerin hiç değerlendirilmediğini ölçer
- for döngüsünün buildArray üzerinden her elemanı her çağrıda değerlendirdiğini gösterir
- Hangi dalın seçileceğinin derlemede değil koşumda belirlendiğini ayırt eder
İçindekiler
Önceki ders bir özelliğin değer kuralını ayrı bir tipe taşıdı. Sonuç kurucu (@resultBuilder) aynı fikri bir ifade listesine uygular. Bir kapanışın gövdesinde alt alta yazılan ifadeler normalde tek tek çalışır ve değerleri atılır. Sonuç kurucu ile işaretlenmiş bir parametreye verilen kapanışta ise her satır kurucu tipin bir yöntemine verilir ve yöntemlerin döndürdüğü değerler birleştirilerek tek bir sonuç elde edilir. Hangi satırın hangi anda çalışacağını, bu yöntemlerden hangisine nasıl bağlandığı belirler.
Bu ders bir dizgi listesi kuran küçük bir kurucu yazar ve üç şeyi ölçer: seçilmeyen if dalındaki ifadenin değerlendirilip değerlendirilmediğini, for döngüsünün elemanları nasıl işlediğini ve dalın seçiminin ne zaman yapıldığını. Dönüşümün kendisi derleme sırasında olur ve koşumda görülmez; ölçülen şey, dönüşmüş kodun koşum sırasındaki davranışıdır. Derleme ile koşum arasındaki bu ayrım önceki bir müfredatta ayrıca ele alınmıştı.
// 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)")
}
}
// ListBuilder.swift — dizgi listesi kuran sonuc kurucu
@resultBuilder
struct ListBuilder {
static func buildExpression(_ line: String) -> [String] {
[line]
}
static func buildBlock(_ parts: [String]...) -> [String] {
parts.flatMap { $0 }
}
static func buildOptional(_ part: [String]?) -> [String] {
part ?? []
}
static func buildEither(first part: [String]) -> [String] {
part
}
static func buildEither(second part: [String]) -> [String] {
part
}
static func buildArray(_ parts: [[String]]) -> [String] {
parts.flatMap { $0 }
}
}
func makeList(@ListBuilder _ content: () -> [String]) -> [String] {
content()
}
Kurucuyu Yazmak
ListBuilder altı statik yöntem taşır. buildExpression her tek ifadeyi bir dizgi listesine çevirir. buildBlock bir bloktaki parçaları art arda birleştirir. buildOptional, else dalı olmayan bir if’in sonucunu karşılar; dal seçilmediyse boş liste verir. buildEither(first:) ve buildEither(second:), if ve else dallarından hangisi seçildiyse onu karşılar. buildArray bir for döngüsünün her yinelemesinden gelen parçaları birleştirir. makeList kapanışı ListBuilder ile işaretlenmiş bir parametre olarak alan yardımcı işlevdir.
Seçilmeyen Dal Değerlendirilmez
İlk ölçüm if/else içinde iki ifade yazar. Biri chosen adlı Probe’u, öteki skipped adlı Probe’u çağırır. Koşul her zaman doğrudur, yani yalnız birinci dal seçilir. render işlevi iki kez çağrılır.
// main.swift — secilmeyen dalin ifadesi degerlendirilmez
let chosen = Probe("secilen-dal")
let skipped = Probe("secilmeyen-dal")
do {
let useFirst = true
func render() -> [String] {
makeList {
"baslik"
if useFirst {
chosen.eval("birinci")
} else {
skipped.eval("ikinci")
}
}
}
for _ in 1...2 {
let lines = chosen.use { skipped.use { render() } }
print(lines.joined(separator: ","))
}
}
chosen.report()
skipped.report()
baslik,birinci baslik,birinci secilen-dal: 2 degerlendirme, 2 kullanim -> her-kullanimda secilmeyen-dal: 0 degerlendirme, 2 kullanim -> hic
İki çağrıda da liste baslik,birinci olur. secilen-dal iki değerlendirme kaydeder, çünkü kurucunun gövdesi her render çağrısında yeniden çalışır. secilmeyen-dal ise iki kullanım görür ama sıfır değerlendirme: ikinci dalın ifadesi hiçbir zaman çalışmaz. Kural hic der. Bu dersin anı hiçtir: seçilmeyen dalın ifadesi hiçbir anda değerlendirilmez. İfadenin yalnızca yazılmış olması, onu çağırmaz.
Döngü Her Elemanı Değerlendirir
for döngüsü buildArray ile birleştirilir ve gövdesindeki ifade her eleman için bir kez değerlendirilir. Ölçüm üç elemanlı bir diziyi iki kez işler ve aynı kapanışa if içeren bir satır da ekler; bu satır buildOptional yoluna girer.
// main.swift — for her elemani degerlendirir
let items = Probe("dongu-elemani")
do {
func render(names: [String]) -> [String] {
makeList {
for name in names {
items.eval(name)
}
if names.count > 2 {
"uzun liste"
}
}
}
for _ in 1...2 {
let lines = items.use { render(names: ["a", "b", "c"]) }
print(lines.joined(separator: ","))
}
}
items.report()
a,b,c,uzun liste a,b,c,uzun liste dongu-elemani: 6 degerlendirme, 2 kullanim -> her-kullanimda
Her çağrıda liste a,b,c,uzun liste olur. dongu-elemani altı değerlendirme kaydeder: iki çağrıda üçer eleman. Elemanlar bir kez önbelleğe alınmadı; kurucunun gövdesi her çağrıda baştan koşar ve döngü her elemanı baştan işler. Anı her-kullanimdadır. if names.count > 2 satırının dizgisi bir Probe’dan geçmedi; yalnızca listede göründü.
İfade Dönüşümü
buildExpression, bloktaki her tek ifadeyi kurucunun parça tipine çevirir. Aynı ada sahip birden çok buildExpression yazılırsa, derleyici ifadenin tipine uyanı seçer. Aşağıdaki MixedBuilder hem String hem Int kabul eder; sayıları sayi 42 biçiminde bir dizgiye çevirir. Ölçüm iki ifadeyi ayrı Probe’lardan geçirir ve her çağrıda her ifadenin kaç kez değerlendirildiğine bakar.
// MixedBuilder.swift — metin ve sayi ifadesi kabul eden sonuc kurucu
@resultBuilder
struct MixedBuilder {
static func buildExpression(_ line: String) -> [String] {
[line]
}
static func buildExpression(_ number: Int) -> [String] {
["sayi \(number)"]
}
static func buildBlock(_ parts: [String]...) -> [String] {
parts.flatMap { $0 }
}
}
func makeMixed(@MixedBuilder _ content: () -> [String]) -> [String] {
content()
}
// main.swift — buildExpression ifadeyi cevirir, yeniden degerlendirmez
let texts = Probe("metin-ifadesi")
let numbers = Probe("sayi-ifadesi")
do {
for _ in 1...2 {
let lines = texts.use {
numbers.use {
makeMixed {
texts.eval("baslik")
numbers.eval(42)
}
}
}
print(lines.joined(separator: ","))
}
}
texts.report()
numbers.report()
baslik,sayi 42 baslik,sayi 42 metin-ifadesi: 2 degerlendirme, 2 kullanim -> her-kullanimda sayi-ifadesi: 2 degerlendirme, 2 kullanim -> her-kullanimda
Çıktı iki çağrıda da baslik,sayi 42 verir: dizgi ifadesi olduğu gibi geçti, tam sayı ifadesi sayi 42 biçimine çevrildi. Çevirinin hangi yoldan yapıldığı ifadenin tipinden anlaşılır; kurucuya hiçbir ayırma yazılmadı. Sayaçlar dönüşümün değerlendirmeyi değiştirmediğini gösterir. metin-ifadesi ve sayi-ifadesi iki çağrıda ikişer değerlendirme kaydeder ve her-kullanimda der. buildExpression ifadenin sonucunu alır, ifadeyi yeniden çalıştırmaz. Kurucu kapanışın gövdesini çağrı başına bir kez baştan koşturur ve her ifade tam bir kez değerlendirilir. Bu yüzden dönüşüm yazarken yan etkili bir ifadenin tekrarlanacağından endişe edilmez. Aynı ad altında birden çok buildExpression yazmak, kurucunun kabul ettiği ifade tiplerini genişletir; tipi karşılamayan bir ifade ise derleme hatası verir, çalışma zamanında sessizce atlanmaz.
İç İçe Koşul
if içindeki if, kurucuda iç içe iki buildOptional yolu açar. Dış koşul tutmazsa iç koşulun kendisi de değerlendirilmez. Ölçüm aynı kapanışı iki kez visible: false ile, sonra iki kez visible: true ile çalıştırır. İç koşul ve iç satırın ifadesi için her durumda ayrı birer Probe vardır.
// main.swift — dis kosul tutmazsa ic kosul degerlendirilmez
let closedGate = Probe("kapali-ic-kosul")
let closedHit = Probe("kapali-ic-satir")
let openGate = Probe("acik-ic-kosul")
let openHit = Probe("acik-ic-satir")
do {
func render(visible: Bool, gate: Probe, hit: Probe) -> [String] {
makeList {
if visible {
if gate.eval(true) {
hit.eval("ic")
}
}
}
}
for _ in 1...2 {
let shut = closedHit.use { closedGate.use { render(visible: false, gate: closedGate, hit: closedHit) } }
let shown = openHit.use { openGate.use { render(visible: true, gate: openGate, hit: openHit) } }
print("kapali \(shut.count), acik \(shown.count)")
}
}
for probe in [closedGate, closedHit, openGate, openHit] {
probe.report()
}
kapali 0, acik 1 kapali 0, acik 1 kapali-ic-kosul: 0 degerlendirme, 2 kullanim -> hic kapali-ic-satir: 0 degerlendirme, 2 kullanim -> hic acik-ic-kosul: 2 degerlendirme, 2 kullanim -> her-kullanimda acik-ic-satir: 2 degerlendirme, 2 kullanim -> her-kullanimda
Dış koşul false iken ne iç koşul ne de iç satır değerlendirilir. kapali-ic-kosul ve kapali-ic-satir ikişer kullanıma karşılık sıfır değerlendirme kaydeder ve hic der; çıktıda kapali 0 satırı da listenin boş kaldığını gösterir. Dış koşul true iken ise iki iç ifade de her çağrıda bir kez değerlendirilir: acik-ic-kosul ve acik-ic-satir ikişer değerlendirme kaydeder ve her-kullanimda der. acik 1 satırı iç satırın listeye girdiğini doğrular. Koşulların sırası burada da kısa devre gibi davranır: sıradaki ifade yalnız öncekinin yolu açıksa değerlendirilir. Fark, bunun && operatöründen değil, kurucunun buildOptional yolundan gelmesidir. Her if kendi buildOptional çağrısını üretir; dıştaki çağrının parçası boşsa içtekine hiç inilmez ve içteki ifade bu yüzden hic kalır.
Boş Döngü
buildArray, döngünün her yinelemesinden gelen parçaları alır. Döngü hiç dönmezse parça listesi boştur ve buildArray boş bir liste üretir. Soru, boş bir dizi üzerinde kurulan döngünün gövdesindeki ifadenin değerlendirilip değerlendirilmediğidir. Ölçüm aynı kapanışı bir kez boş, bir kez dolu bir dizi ile çalıştırır.
// main.swift — bos dizi uzerinde dongu govdesi degerlendirilmez
let emptyLoop = Probe("bos-dongu")
let filledLoop = Probe("dolu-dongu")
do {
func render(names: [String], probe: Probe) -> [String] {
makeList {
for name in names {
probe.eval(name)
}
}
}
for _ in 1...2 {
let none = emptyLoop.use { render(names: [], probe: emptyLoop) }
let some = filledLoop.use { render(names: ["x", "y"], probe: filledLoop) }
print("bos \(none.count), dolu \(some.count)")
}
}
emptyLoop.report()
filledLoop.report()
bos 0, dolu 2 bos 0, dolu 2 bos-dongu: 0 degerlendirme, 2 kullanim -> hic dolu-dongu: 4 degerlendirme, 2 kullanim -> her-kullanimda
Boş dizi üzerindeki döngüde gövdenin ifadesi hiç çalışmaz: bos-dongu iki kullanıma karşılık sıfır değerlendirme kaydeder ve hic der. bos 0 satırı sonucun da boş liste olduğunu söyler; buildArray boş parça listesini boş bir sonuca çevirir ve hata vermez. Dolu dizide her çağrı iki eleman işler, dolayısıyla dolu-dongu iki çağrıda dört değerlendirme kaydeder ve her-kullanimda der. Değerlendirme sayısı döngünün dönüş sayısına eşittir. Kurucu gövdesinde döngü kuran biri için bu bir ölçüdür: elemanları yan etkili bir ifadeyle işleyen döngü, her çağrıda eleman sayısı kadar koşar. Koleksiyon çağrılar arasında saklanmaz; yan etkinin tekrarı gerekmiyorsa döngü kurucunun dışına alınır ve sonuç kurucuya hazır verilir.
Bu ölçümlerin ortak dersi şudur: kurucu, gövdedeki her satırı bir çağrıya çevirir ve hangi çağrının yapılacağını yazım biçimi belirler. Bir ifadenin değerlendirilip değerlendirilmeyeceğini okuyarak tahmin etmek yerine, dönüşümü zihinde açmak gerekir: ifade bir buildExpression çağrısına, koşul bir buildOptional (else varsa buildEither) çağrısına, döngü bir buildArray çağrısına dönüşür. Yan etkisi olan bir ifadeyi gövdeye koymadan önce bu çağrı zincirinde kaç kez ve hangi koşulda çalışacağı sorulur.
Sınırlayıcı Ölçüm
Dönüşüm derlemede yapılır, ama hangi dalın seçileceği koşumda belirlenir. Önceki iki ölçümde koşul sabitti ve bu fark görünmüyordu. Son ölçüm koşulu bir parametre yapar ve render’ı sırayla true, false, true ile çağırır. Her iki dalın ifadesi ayrı birer Probe’a yazılır.
// main.swift — dal secimi kosumda yapilir
let left = Probe("sol-dal")
let right = Probe("sag-dal")
do {
func render(first: Bool) -> [String] {
makeList {
if first {
left.eval("sol")
} else {
right.eval("sag")
}
}
}
for flag in [true, false, true] {
let lines = left.use { right.use { render(first: flag) } }
print(lines.joined(separator: ","))
}
}
left.report()
right.report()
sol sag sol sol-dal: 2 degerlendirme, 3 kullanim -> her-kullanimda sag-dal: 1 degerlendirme, 3 kullanim -> ilk-kullanimda
Çağrılar sırasıyla sol, sag, sol yazar. sol-dal iki değerlendirme kaydeder: yalnız true ile gelen iki çağrıda koşar. sag-dal ise tek değerlendirme kaydeder ve bu değerlendirme ikinci çağrıda, yani ilk kullanımdan sonra olduğu için kural ilk-kullanimda der. İfade, dalın seçildiği çağrıda değerlendirilir; kodda yazılı olmasının tek başına bir anlamı yoktur. Bu yüzden hic anı yalnızca dal hiç seçilmediğinde geçerlidir ve koşula bağlıdır.
BL5. Ölçülen kurucu yalnız dizgi listesi kurar ve buildLimitedAvailability gibi ek yöntemler tanımlanmadı. İki dallı if, üç dallı if/else if zincirlerinde aynı kuralın geçtiği ölçülmedi. Tek bir kurucu tipinin kullanılması tasarım tercihidir.
Özet
- Sonuç kurucu, kapanış gövdesindeki ifadeleri
buildBlock,buildExpression,buildOptional,buildEithervebuildArrayyöntemleriyle bir sonuca birleştirir. if/elseiçinde seçilmeyen dalın ifadesi hiç değerlendirilmez; yazılmış olması onu çağırmaz.- Kurucunun gövdesi her çağrıda baştan koşar ve
fordöngüsü her elemanı her çağrıda değerlendirir. - Dönüşüm derlemede yapılır, ama hangi dalın seçileceği koşumda belirlenir;
hicanı yalnız dal hiç seçilmezse geçerlidir. - Bu dersin anı: hiç. Seçilmeyen dalın ifadesi hiçbir anda değerlendirilmez.
Sonraki Adım
Bu konunun ilk dersleri tek bir akışta çalışan koddaki bellek ve değerlendirme anlarını ölçtü. Birden çok iş aynı anda aynı belleğe dokunduğunda yeni sorular çıkar: bir değer kimin kontrolünde, kim ona ne zaman erişir. Sonraki ders aktörü, aktarılabilir tip anlamına gelen Sendable’ı ve @MainActor’ı anlatır; aktörün tembel özelliğinin ilk erişimde nasıl davrandığını ve async let ile başlatılan işi ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.