İçeriğe geç
academia.sh

Ders 04 / 21

Dizgi İşlemleri

`String` değerinin karakter ve bayt sayısını, dizinlemeyi ve ifade yerleştirmeyi ölçer: yerleştirilen ifade her kullanımda yeniden kurulur.

Yayın Güncelleme

Bu derste

  • `count` ile `utf8.count` arasındaki farkı karakter modeliyle açıklamak
  • Dizgide tam sayı yerine `String.Index` kullanılmasının nedenini söylemek
  • İfade yerleştirmenin her değerlendirmede `description` özelliğini yeniden çağırdığını göstermek
  • Saklanan dizgi ile yeniden kurulan dizgiyi ayırt etmek
İçindekiler

String bir önceki derste yalnız adıyla geçti. Bu derste üç soru var: bir dizgide kaç karakter vardır, dizginin belirli bir karakterine nasıl ulaşılır, ve dizgiye yerleştirilen bir ifade kaç kez kuruluyor? Üçüncüsü kursun ekseninde durur; ilk ikisi onu anlamanın zeminidir.

Karakter Modeli

Swift’te dizgi Character değerlerinden oluşur ve count karakter sayar. Dizginin bellekteki bayt sayısı ise utf8.count ile ayrıca sorulur. İkisi aynı olmak zorunda değildir: Türkçe harflerin bir kısmı birden çok baytla saklanır.

Dizgiye tam sayıyla da erişemezsin. word[1] yazmak Swift 6.4’te derleyicide reddedilir: aşağıdaki parça derlenmez, bu yüzden çıktısızdır. Neden: karakterler bellekte farklı sayıda bayt tuttuğu için n. karakterin yeri baştan sayılmadan bulunamaz.

let word = "ağaç"
print(word[1])

Doğrusu String.Index kullanmaktır: word.startIndex ilk karakteri gösterir, word.index(after:) bir sonrakini. Ölçüm bunu da kapsar.

İfade Yerleştirme

Dizgi içine "\(değer)" yazmak ifade yerleştirmedir (string interpolation). CustomStringConvertible uyan bir tip, yerleştirildiği her yerde kendi description özelliğini sorar. İfade yerleştirme dizgiyi kuran bir ifadedir; kurulduğu her seferde description yeniden çağrılır.

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

Ölçüm, description özelliğinin içinde bir Probe taşıyan küçük bir tip kurar. İki dizgi yerleştirmesi aynı değeri iki kez kullanır; ayrıca Counter tipiyle saklanan dizginin sonradan değişmediği gösterilir.

// main.swift — karakter modeli ve ifade yerlestirme
struct Tag: CustomStringConvertible {
    let probe: Probe
    var description: String { probe.eval("etiket") }
}

struct Counter: CustomStringConvertible {
    var n: Int
    var description: String { "n=\(n)" }
}

let word = "ağaç"
print("ağaç: \(word.count) karakter, \(word.utf8.count) bayt")
let second = word.index(after: word.startIndex)
print(word[second])

let descriptionProbe = Probe("description-cagrisi")
let savedProbe = Probe("saklanan-dizgi")

let tag = Tag(probe: descriptionProbe)
_ = descriptionProbe.use { "ilk: \(tag)" }
_ = descriptionProbe.use { "ikinci: \(tag)" }

let saved = Tag(probe: savedProbe)
let text = "\(saved)"
_ = savedProbe.use { text }
_ = savedProbe.use { text }

var counter = Counter(n: 1)
let snapshot = "\(counter)"
counter.n = 2
print("\(snapshot) \(counter)")

let allProbes = [descriptionProbe, savedProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
ağaç: 4 karakter, 6 bayt
ğ
n=1 n=2
description-cagrisi: 2 degerlendirme, 2 kullanim -> her-kullanimda
saklanan-dizgi: 1 degerlendirme, 2 kullanim -> hemen
hemen: 1
ilk-kullanimda: 0
her-kullanimda: 1
hic: 0

Dört harfli ağaç dört karakterdir ama altı bayttır: ğ ve ç ikişer bayt tutar. word[second] ikinci karakteri, ğ’yi verir; dizin bir sayı değil, dizgiyi bilen bir değerdir.

description-cagrisi iki kullanımda iki değerlendirme gösterdi: her ifade yerleştirme, değeri istendiği anda yeniden kurdu. Kural bunu her-kullanimda okur. Dizgi kuran ifade tembel bir ifade değil, her yazıldığı yerde çalışan bir ifadedir.

DT24. ağaç hizalı sütuna konmadı ve dizede olduğu gibi durur: ölçümün konusu bu sözcüğün karakter ve bayt sayısıdır.

DT25. count ve utf8.count değerleri bu sözcük için ölçüldü; başka bir sözcüğün ya da başka bir yazı sisteminin sayıları bu dersin iddiası değildir.

Birleştirme ve Çok Satırlı Dizgi

Dizgileri + işleciyle birleştirebilirsin: "a" + "b" yeni bir dizgi üretir. Birden çok satıra yayılan metin için üç tırnaklı çok satırlı dizgi değişmezi vardır: açılış """ kendi satırında durur, metin sonraki satırlarda başlar ve kapanış """ yine kendi satırında biter. Kapanış üç tırnağın girintisi, bütün satırlardan çıkarılır; böylece metni kodun girintisine uydurarak yazarsın ve çıktıda fazladan boşluk kalmaz.

Bir dizginin bir ifadeye göre biçimlenmesi, ifadenin her çağrıda yeniden değerlendirilmesi demektir. Aşağıdaki blok bir fonksiyon yazar; fonksiyon gelen metni köşeli parantezle sarar ve sarma işlemini bir Probe üzerinden geçirir. Fonksiyon iki kez çağrılır. Son satırda çok satırlı bir değişmez yazdırılır.

// Join.swift — birlestirme ve cok satirli dizgi
func decorate(_ text: String, probe: Probe) -> String {
    probe.eval("[" + text + "]")
}
let joinProbe = Probe("birlestirme")
print(joinProbe.use { decorate("a", probe: joinProbe) })
print(joinProbe.use { decorate("b", probe: joinProbe) })
let note = """
    ilk satir
      ikinci satir
    """
print(note)
joinProbe.report()
[a]
[b]
ilk satir
  ikinci satir
birlestirme: 2 degerlendirme, 2 kullanim -> her-kullanimda

İlk iki satır [a] ve [b] oldu. Sonraki iki satır çok satırlı dizginin çıktısıdır: ilk satir satırı girintisiz, ikinci satir satırı iki boşluk girintili basıldı. Kapanış """ dört boşluk girintiliydi, bu girinti bütün satırlardan çıkarıldı; ikinci satırın kendi fazladan iki boşluğu ise korundu. Ölçüm satırı iki değerlendirme gösteriyor: decorate çağrısı gövdesinde birleştirmeyi her seferinde yeniden hesaplar, yani an her-kullanimda olur.

Çok satırlı dizgi değişmezi ise bir ifade olarak yalnız bir kez okunur; yazılan metin bir sabittir ve çalışma sırasında birleştirilmez. Birleştirmeyi + ile yaptığında ise birleştirme çalışma sırasında yapılabilir. Hangisini seçeceğin metnin sabit mi, hesaplanan mı olduğuna bağlıdır.

DT26. Birleştirme ve çok satırlı değişmez ASCII metinle ölçüldü; satır sonu karakterlerinin platforma göre farkı bu dersin dışındadır.

Substring Dilimdir, Dizgi Değildir

Bir dizgiden bir parça aldığında, örneğin prefix(7) ya da bir aralıkla dilimleme yaptığında, sonuç String değil Substring tipindedir. Substring, kaynak dizgi üzerinde bir dilim görünümüdür ve dizginin pek çok yöntemini taşır. Bir Substring’i kalıcı olarak saklamak ya da bir String bekleyen yere vermek istediğinde onu String(...) ile dizgiye çevirirsin. Bu açık dönüşüm, kısa ömürlü bir dilimin kaynak dizgiyi gereksiz yere tutmasını önler.

Aşağıdaki blok bir cümleyi bir Probe ile üretir, ondan bir dilim alır, dilimin tipini yazdırır ve dilimi String yapar. Kaynak dizginin değerlendirme sayısı dilimleme ve dönüştürme boyunca izlenir.

// Slice.swift — dilim ve dizgiye donusum
let sourceProbe = Probe("kaynak-cumle")
let sentence = sourceProbe.eval("merhaba dunya")
let head = sentence.prefix(7)
let owned = String(head)
_ = sourceProbe.use { head.count }
_ = sourceProbe.use { owned.count }
print(head, type(of: head))
print(owned, type(of: owned))
sourceProbe.report()
merhaba Substring
merhaba String
kaynak-cumle: 1 degerlendirme, 2 kullanim -> hemen

head merhaba yazdı ve tipi Substring çıktı; String(head) ise aynı yedi karakterli metni String olarak taşıdı. Probe tek değerlendirme gösterdi: dilimleme ve dönüştürme kaynak dizgiyi yeniden üretmedi, var olan değer üzerinde çalıştı. Dilim kaynağın bir parçasına bakar; dönüştürme ise o parçanın bağımsız bir dizgi kopyasını verir. Değerlendirme kullanımdan önce tek kez yapıldığı için an hemen olur.

Bu ayrım API’lerde kendini gösterir: bir fonksiyon String bekliyorsa, dilimi doğrudan veremezsin, önce dönüştürmen gerekir. Derleyicinin bunu zorlaması, dilimin ömrünü bilinçli yönetmeni sağlar.

DT27. Dilim prefix ile ölçüldü; aralık işleciyle dilimlemenin ve dilim üzerinde başka işlemlerin aynı tipi verdiği bu dersin dışındadır.

Karakter, Unicode Skaları ve Bayt

Dizgi sayımlarında üç ayrı birim vardır. Karakter (Character), okuyanın tek bir simge olarak gördüğü birimdir. Unicode skaları, Unicode tablosundaki tek bir kod noktasıdır. Bayt ise UTF-8 kodlamasında dizginin saklandığı birimdir. Çoğu ASCII karakter için üçü de birdir; ama birleşik karakterlerde ayrışırlar. Örneğin e harfi ile ona eklenen vurgu işareti iki skalar olarak yazılır, buna rağmen tek karakter olarak görünür.

Aşağıdaki blok e harfini ve onu izleyen birleştirici vurgu işaretini \u{301} kaçış dizisiyle yazar. Dizginin üç sayımı, bir fonksiyonun içinde bir Probe üzerinden ölçülür. Fonksiyon iki farklı metinle çağrılır; ikinci metin düz ASCII bir kelimedir.

// Units.swift — karakter, skalar ve bayt sayilari
func measure(_ text: String, probe: Probe) -> (Int, Int, Int) {
    probe.eval((text.count, text.unicodeScalars.count, text.utf8.count))
}
let unitProbe = Probe("dizgi-birimleri")
let combined = "e\u{301}"
print(unitProbe.use { measure(combined, probe: unitProbe) })
print(unitProbe.use { measure("kedi", probe: unitProbe) })
unitProbe.report()
(1, 2, 3)
(4, 4, 4)
dizgi-birimleri: 2 degerlendirme, 2 kullanim -> her-kullanimda

Birleşik metin (1, 2, 3) verdi: bir karakter, iki Unicode skaları, üç bayt. e harfi UTF-8 kodlamasında bir bayt, birleştirici işaret iki bayt tutar. Düz kedi kelimesinde ise üç sayı dört, dört ve dört çıktı. İki çağrı iki değerlendirme getirdi, bu yüzden an her-kullanimda okunur.

Karakter sayısını okuyanın gördüğü simgelerle ilişkilendirebilirsin, ama bayt sayısı depolama boyutuna bakar. İkisini karıştırmak, sınır denetimlerinde sessiz hataya yol açar. Bir dizginin uzunluğunu sorarken hangi birimi sorduğunu seç.

DT28. Üç sayım iki metinle ölçüldü; diğer birleşik karakter ve emoji dizilerinin sayımları bu dersin dışındadır.

Karşılaştırma, Önek ve Sonek

== iki dizgiyi karakter karakter karşılaştırır, ama “aynı karakter” kavramı Unicode’a göredir: farklı skalar dizileriyle yazılmış ama okuyana aynı görünen iki dizgi, kurallı eşdeğerlikle eşit sayılır. Bu, tek bir é ile e ve birleştirici vurgu işaretinden oluşan dizgiyi aynı yapar. hasPrefix ve hasSuffix ise dizginin başını ve sonunu denetler; onlar da aynı karakter anlayışını kullanır.

Aşağıdaki blok iki yazımı karşılaştırır. Tek skalarlı yazım bir Probe’dan geçer, ikincisi iki skalarlıdır. Sonra hasPrefix ve hasSuffix sonuçları yazdırılır.

// Compare.swift — karsilastirma, onek ve sonek
func same(_ left: String, _ right: String, probe: Probe) -> Bool {
    probe.eval(left == right)
}
let compareProbe = Probe("dizgi-esitligi")
let single = "\u{E9}"
let composed = "e\u{301}"
print(compareProbe.use { same(single, composed, probe: compareProbe) })
print(single.unicodeScalars.count, composed.unicodeScalars.count)
let word = "swift-dili"
print(word.hasPrefix("swift"), word.hasSuffix("dil"))
compareProbe.report()
true
1 2
true false
dizgi-esitligi: 1 degerlendirme, 1 kullanim -> ilk-kullanimda

Tek skalarlı é ile iki skalarlı e\u{301} karşılaştırması true verdi, ama skalar sayıları 1 ve 2 olarak ayrıştı. Yani == skalara değil, kurallı eşdeğerliğe bakar: iki dizgi bayt bayt aynı olmasa da aynı karakteri temsil ediyorsa eşit sayılır. hasPrefix("swift") doğru, hasSuffix("dil") yanlış çıktı, çünkü kelime dili ile bitiyor, dil ile değil. Probe tek değerlendirme gösterdi ve an ilk-kullanimda olur.

Bu, kullanıcı girdisini karşılaştıran kodda önemlidir: klavyeden ya da bir dosyadan gelen metin, aynı görünen ama farklı skalarlarla yazılmış olabilir. == bunu senin yerine çözer; ham bayt karşılaştırması çözmezdi.

DT29. Karşılaştırma tek bir birleşik karakterle ölçüldü; büyük küçük harf duyarsız karşılaştırma bu dersin dışındadır.

Dizinle Erişim

Swift dizgisine text[3] gibi bir tam sayıyla erişemezsin. Karakterlerin bayt uzunluğu değiştiği için üçüncü karakterin nerede başladığı, baştan sayılmadan bilinemez. Bunun yerine String.Index değerleri kullanılır: text.startIndex ilk karakteri, text.index(text.startIndex, offsetBy: 3) dördüncü karakteri gösterir. Dizin hesabı dizginin içinde yürüyerek yapılır.

Aşağıdaki blok bir kelimeden üçüncü karakteri dizinle alır. Kelime bir Probe’dan gelir; fonksiyon iki farklı kelimeyle çağrılır.

// Index.swift — dizinle erisim
func third(_ text: String, probe: Probe) -> Character {
    let source = probe.eval(text)
    let position = source.index(source.startIndex, offsetBy: 2)
    return source[position]
}
let indexProbe = Probe("dizin-erisimi")
print(indexProbe.use { third("kalem", probe: indexProbe) })
print(indexProbe.use { third("defter", probe: indexProbe) })
indexProbe.report()
l
f
dizin-erisimi: 2 degerlendirme, 2 kullanim -> her-kullanimda

kalem kelimesinde üçüncü karakter l, defter kelimesinde f çıktı. Dizin her kelimede baştan yürünerek bulundu: index(_:offsetBy:) karakterleri tek tek geçti, çünkü bir karakterin kaç bayt tuttuğu önceden bilinmiyor. Tam sayı alt simgesi bu yüzden yoktur; olsaydı, uzun bir metinde sessizce yavaş bir arama gizlenirdi. Probe iki çağrıya karşılık iki değerlendirme gösterdi ve an her-kullanimda okunur.

Dizinle erişim, sık yapılan bir işse bir Array(text) dönüşümüyle karakter dizisine çevrilip tam sayıyla okunabilir; ama bu, dizginin tamamını bir kez kopyalamak demektir.

Dizin tipinin ayrı bir tip olması bir de güvenlik sağlar: bir dizgiden alınan dizini başka bir dizgide kullanmak hatalı sonuç verebilir. Dizini hesaplayan satır ile onu kullanan satırı aynı dizgiye bağlı tutmak bu yüzden iyi bir alışkanlıktır. Aynı fonksiyonda source adıyla tek bir yerel kopya kullanmak, dizinin ve dizginin aynı değere bakmasını güvenceye alır.

Bir dizginin sonundaki karaktere erişmek için de aynı mekanizma geçerlidir: text.index(before: text.endIndex) son karakterin dizinini verir. Dizin hesapları baştan ya da sondan yürüdüğü için, uzun dizgilerde bunları bir döngünün içinde tekrar tekrar yapmak yerine bir kez hesaplayıp saklamak daha ucuzdur. Ölçüm, kelimenin her çağrıda yeniden alındığını gösteriyor.

DT30. Dizin erişimi ASCII kelimelerle ölçüldü; sınır dışı dizinin durdurduğu durumlar bu derste çalıştırılmaz.

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

Dizgi bir kez kurulup bir let içinde saklanırsa tablo değişir. saklanan-dizgi iki kullanıma karşın bir değerlendirme gösterdi: "\(saved)" yalnız saklandığı satırda çalıştı, sonraki iki okuma yalnız dizgiyi okudu. Kural hemen der.

Bu bir bedeli de getirir. snapshot dizgisi counter değişmeden önce kuruldu, ve dizgi kurulduktan sonra değişen değer ona yansımaz: snapshot hâlâ n=1, yeniden kurulan dizgi n=2. Saklanan dizgi bir fotoğraftır; ifade yerleştirme ise her seferinde yeniden çekilen bir fotoğraftır.

DT31. Counter bir yapıdır ve snapshot onun kurulduğu andaki değeri taşır; sınıf tiplerinin davranışı bu derste ölçülmedi.

Özet

  • count karakter sayar, utf8.count bayt sayar; ağaç 4 karakter ve 6 bayttır.
  • Dizgi tam sayıyla dizinlenmez; String.Index kullanılır.
  • İfade yerleştirme, description özelliğini her değerlendirmede yeniden çağırır.
  • Saklanan dizgi tek bir değerlendirmedir ve sonradan değişen değeri yansıtmaz.
  • Bu dersin anı: her kullanımda (iki yerleştirmede iki description çağrısı).

Sonraki Adım

Dizgiler + ve == gibi işleçlerle de birleşir ve karşılaştırılır. Sıradaki ders işleçlere bakıyor: bir işlecin sağ tarafı her zaman değerlendirilir mi?

İ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