Swift dizgesine eklenen metni izlemek için eski dizge indekslerini saklamak yerine, adlandırılmış bir kodlamada ofsetleri saklayın. Bu kılavuz, küçük ve yerel bir Mac metin önizleme hesaplaması için yalnızca ekleme yapan bir UTF-16 parça defteri oluşturur.

Görev belirli: sentetik metin parçalarını birleştirmek, her parçanın nereye ait olduğunu hatırlamak ve sonraki eklemelerden sonra parçanın tam içeriğini geri almak. Örnek tamamen bellekte çalışır ve sonuçları yazdırır. Parça sınırları, metne yapılan katkıları tanımlar; bunlar görüntülenen karakterlerin sınırlarından farklı olabilir.

Eski bitiş indeksini kaydetmek neden güvenilir değildir?

Cazip bir yaklaşım, text.endIndex değerini kaydeder, bir parça ekler ve bu kayıtlı indeksi yeni text.endIndex ile eşleştirir. Bu, değişiklikten önce elde edilmiş bir indeksi değişiklikten sonraki bir dizgeyle karıştırır. Swift’in koleksiyon belgeleri, değişiklik yapan işlemlerin kayıtlı indeksleri geçersiz kılabileceği konusunda açıkça uyarır. Kısa bir örnekle başarılı sonuç almak, sonraki değişiklikler için genel bir sözleşme oluşturmaz. Apple’ın Collection belgelerine bakın.

İndeksleri yeniden oluştursanız bile ikinci bir sorun vardır: metin eklemek, birleşme noktasındaki karakter sınırını değiştirebilir. Swift’in dil kılavuzu bunu "cafe" ve ardından birleştirici bir akut aksan olan "\u{0301}" örneğiyle gösterir. Aksan önceki harfe katıldığı için sonuçta hâlâ dört Swift Character değeri bulunur. Strings and Characters bölümüne bakın.

Dolayısıyla, eski karakter sayısını kaydedip bitmiş dizgede bu kadar karakter ilerlemek, eklenen parçanın başlangıcını güvenilir biçimde bulmaz. Aksan örneğinde, eklenen katkı son bileşik grafem kümesinin içinde başlar. Bu katkıya karşılık gelen ayrı bir görüntülenen karakter yoktur.

Ekleme sınırı ile karakter sınırını ayrı kavramlar olarak ele alın. İlki metnin nasıl oluşturulduğunu kaydeder; ikincisi Swift’in oluşturulmuş metni karakter düzeyindeki işlemler için nasıl gruplandırdığını açıklar.

Defter hangi koordinatları saklamalı?

Bu Mac hesaplaması için tamsayı UTF-16 ofsetleri ve uzunlukları kullanın. Apple, NSString dizge konumlarının ve aralıklarının UTF-16 kod birimleriyle ifade edildiğini belgeliyor. Swift’in String.utf16 görünümü eşleşen kod birimlerini sunduğundan, defter hem ölçüm hem de Foundation alt dize çıkarımı için tek bir koordinat sistemi kullanabilir. NSString ve String’in UTF-16 görünümü bölümlerine bakın.

Okuyucunun görevi Uygun gösterim Koşul
Özgün parçaların her birini bağımsız olarak geri almak Sahip olunan String değerlerinden oluşan bir dizi Birleştirilmiş metindeki koordinatlara gerek yoktur
Birleştirilmiş metindeki tam katkıları bulmak UTF-16 ofsetleri ve uzunlukları Önceki metin değişmeden kalır
Görüntülenen karakterlerle çalışmak Geçerli dizgede yeni karakter indeksleri Parça sınırları grafem sınırları olarak yeniden değerlendirilmelidir
Konumları gelişigüzel düzenlemeler boyunca korumak Düzenlemeleri izleyen bir konum modeli Bu ekleme odaklı defter yetersizdir

Aşağıdaki defterin dört açık kuralı vardır:

  1. Boş bir dizgeyle başlar.
  2. Tek metin değişikliği, tam bir Swift String değerinin eklenmesidir.
  3. Her eklemede önceki UTF-16 toplamı ve parçanın UTF-16 uzunluğu kaydedilir.
  4. Bir parça numarası yalnızca onu oluşturan defter örneği veya anlık görüntüyle birlikte anlam taşır.

Bu kurallar ofsetleri kullanışlı kılar. Sonek eklemek, önceki kodlanmış içeriği yerinde bırakır; böylece eski aralıklar konumlarını korur. Önceki metni eklemek, kaldırmak, değiştirmek veya dönüştürmek farklı bir sözleşme gerektirir. Bu tasarım, Swift’in otomatik konum izleme özelliği değildir; belgelenmiş dizge gösterimlerinden türetilmiştir.

Eksiksiz bir yerel parça defteri oluşturun

Foundation’ın kullanılabildiği bir macOS Swift komut satırı hedefi kullanın. Örnek üst düzey programı aşağıdaki eksiksiz programla değiştirin. Uygulama, belgelenmiş Swift imzası başka bir String kabul eden append(contentsOf:) öğesini kullanır. Apple’ın append(contentsOf:) başvurusuna bakın.

import Foundation

struct Segment: Sendable {
    let utf16Location: Int
    let utf16Length: Int

    var foundationRange: NSRange {
        NSRange(location: utf16Location, length: utf16Length)
    }
}

enum LedgerError: Error {
    case unknownSegment(Int)
}

struct SegmentLedger: Sendable {
    private(set) var text = ""
    private(set) var segments: [Segment] = []
    private var utf16Total = 0

    @discardableResult
    mutating func append(_ fragment: String) -> Int {
        let length = fragment.utf16.count
        let (newTotal, overflow) =
            utf16Total.addingReportingOverflow(length)
        precondition(!overflow, "UTF-16 total exceeds Int capacity")

        let segment = Segment(
            utf16Location: utf16Total,
            utf16Length: length
        )

        text.append(contentsOf: fragment)
        segments.append(segment)
        utf16Total = newTotal
        return segments.count - 1
    }

    func fragment(at segmentID: Int) throws -> String {
        guard segments.indices.contains(segmentID) else {
            throw LedgerError.unknownSegment(segmentID)
        }

        let segment = segments[segmentID]
        if segment.utf16Length == 0 {
            return ""
        }

        return (text as NSString).substring(
            with: segment.foundationRange
        )
    }
}

let inputs = ["cafe", "\u{0301}", "", "\u{1F7E6}", " done"]
var ledger = SegmentLedger()

for input in inputs {
    ledger.append(input)
}

let expectedLocations = [0, 4, 5, 5, 7]
let expectedLengths = [4, 1, 0, 2, 5]

for id in inputs.indices {
    let recovered = try ledger.fragment(at: id)
    let segment = ledger.segments[id]

    // Compare code units, so the check is encoding-exact.
    precondition(Array(recovered.utf16) == Array(inputs[id].utf16))
    precondition(segment.utf16Location == expectedLocations[id])
    precondition(segment.utf16Length == expectedLengths[id])

    print("\(id): location=\(segment.utf16Location), " +
          "length=\(segment.utf16Length)")
}

precondition(ledger.text.utf16.count == 12)
precondition(ledger.text.count == 10)

// Later appends must preserve earlier segment contents.
ledger.append("!")
for id in inputs.indices {
    let recovered = try ledger.fragment(at: id)
    precondition(Array(recovered.utf16) == Array(inputs[id].utf16))
}

// Invalid segment numbers produce an explicit error.
do {
    _ = try ledger.fragment(at: -1)
    preconditionFailure("Expected an unknown-segment error")
} catch LedgerError.unknownSegment(let id) {
    precondition(id == -1)
}

print("All segment checks passed.")

Bu program, alınan API belgeleriyle karşılaştırılarak incelendi; kullanılabilir kabukta Swift yürütülebilir dosyası bulunmadığı için burada çalıştırılmadı. İçindeki önkoşullar, Mac’inizde çalıştırdığınızda örneğin beklenen davranışını açıkça belirtir.

Örneğin denetlemek üzere tasarlandığı noktalar

Beklenen parça koordinatları, sabit metin girdilerinden hesaplanmıştır. Birleştirici aksan bir UTF-16 kod birimi, boş parça sıfır, mavi kare ise iki kod birimi katkıda bulunur. Son sonek beş birimdir. Bunlar örnek girdi hesaplamalarıdır; performans ölçümleri değildir.

Parça Girdi UTF-16 konumu UTF-16 uzunluğu
0 "cafe" 0 4
1 "\u{0301}" 4 1
2 "" 5 0
3 "\u{1F7E6}" 5 2
4 " done" 7 5

Boş parça, sonraki parçayla aynı konumu paylaşmasına rağmen kendi parça numarasını alır. Bu, metin uydurmadan ekleme geçmişini korur. Ayrıca "" değerini açıkça döndürmek, bu durum için Foundation alt dize çağrısı yapılmasını önler.

Geri alma denetimi UTF-16 dizilerini özellikle karşılaştırır. Swift dizge eşitliği kanonik eşdeğerliği tanır; yani temel skaler dizileri farklı olan bazı dizgeler eşit karşılaştırılır. Kodlama düzeyinde tam eşleşme denetimi, bu defterin sözleşmesine daha uygundur. Strings and Characters içindeki dizge karşılaştırması açıklamasına bakın.

Çıkarma işlemi neden Foundation kullanıyor?

Saklanan tamsayılar bir NSRange değerine dönüştürülür ve doğrudan NSString.substring(with:) öğesine aktarılır. Apple, bu yöntemin belirtilen aralığı çıkardığını ve aralığın alıcının sınırları içinde kalması gerektiğini belgeliyor. Geçersiz aralıklar istisnaya neden olabilir. Defter hem metni hem de aralıklarını kontrol eder ve çıkarma işleminden önce bilinmeyen parça numaralarını reddeder. substring(with:) bölümüne bakın.

Kaydedilen her sınır, tam bir Swift dizgesinin eklenmesinden gelir. Örnek, mavi karenin UTF-16 vekil çiftinin ortasında hiçbir zaman yapay bir ofset oluşturmaz. Aksanın gösterdiği gibi, bir sınır yine de bileşik bir karakter dizisinin içinde olabilir; bu gösterimin amacı katkıları tam olarak geri almaktır.

Foundation aralıklarını oluşturmak için String.count kullanmayın. Apple, NSString.length değerini UTF-16 kod birimi sayısı olarak tanımlar. Bu, karakter sayısından farklı bir ölçümdür ve görüntülenen genişliği belirtmez. NSString.length bölümüne bakın.

Değişikliği, parça oluşturmayı ve anlık görüntüleri birlikte tutun

Küçük ve eşzamanlı bir hesaplama için değer türü yeterlidir: tek bir sahip parçaları ekler ve sonucu okur. Özel ayarlayıcılar, çağıranların birleştirilmiş metni parça tablosundan bağımsız olarak değiştirmesini önler. Önbelleğe alınan UTF-16 toplamı da her eklemede büyüyen birleştirilmiş dizgenin yeniden sayılmasını önler.

Birden fazla eşzamansız görev tek bir deftere katkıda bulunacaksa, defteri tek bir actor arkasına koyun ya da Mac arayüzüne aitse @MainActor üzerinde tutun. Ekleme ve üstveri güncellemesini tek bir eşzamanlı yalıtılmış yöntemde gerçekleştirin. Swift, actor yalıtımlı eşzamanlı işin, yöntem içindeki bir askıya alma noktasında kesintiye uğratılamayacağını belgeliyor. Concurrency bölümüne bakın.

Metni ve parça üstverisini tek bir değer anlık görüntüsü olarak birlikte döndürün. Metin okuması ile parça tablosu okuması için ayrı ayrı await kullanmaktan kaçının: bu çağrılar arasında başka bir ekleme gerçekleşebilir. Örnekteki Sendable bildirimleri, yalıtım sınırları arasında aktarılmaya uygun değerleri tanımlar; paylaşılan bir değişkene eşzamanlı yazmaları sıraya koymaz. Swift’in Sendable belgeleri, aktarılabilir değerlerle korunan değiştirilebilir durum arasındaki farkı açıklar.

Katkıların sırası önemliyse, ekleme yapmadan önce bu sırayı belirleyin. Bir actor defterin iç tutarlılığını korur; uygulamanın yine de hangi parçanın önce geleceğine karar vermesi gerekir.

Bu ofsetlerin görevi ne zaman yerine getirmediğini bilin

Bir aralığı yeniden kullanmadan önce şu tanılama adımlarını izleyin:

  1. Sahipliği denetleyin. Parça numarası bu deftere mi, yoksa onun ekleme odaklı uyumlu bir devamına mı ait?
  2. Değişiklik geçmişini denetleyin. Kaydedilen aralıktan önce herhangi bir şey değişti mi? Değiştiyse defteri yeniden oluşturun veya düzenlemeleri izleyen bir model benimseyin.
  3. Birimleri denetleyin. Tüketicinin koordinatları UTF-16 kod birimleri mi? Karakter sayısı ya da UTF-8 bayt sayısını kullanmayın.
  4. İstenen sonucu denetleyin. Eklenen katkıyı tam olarak mı geri alıyorsunuz, yoksa görüntülenen karakterin tamamını mı seçiyorsunuz? Bunlar farklı sınırlar gerektirebilir.

Karakter odaklı bir seçim, her zaman yalnızca tek bir eklenen katkıyı temsil edemez. Aksan, önceki harfle aynı görüntülenen karaktere aittir. Masaüstü görevi vurgulama ise açık bir sunum kuralı seçin: seçimi kapsayan karaktere genişletin veya katkıyı ayrı bir parça listesinde gösterin. Genişletme, önceki bir parçadan metni de içerir; bu nedenle tam geri alma için özgün ofsetleri saklayın.

Ekleme işlemlerini tam olarak izlemek için kullanışlı değişmez basittir: öneki değiştirmeyin, koordinat birimlerini belirtin ve her aralığı metniyle birlikte tutun. Bu sözleşme altında defter, tek bir String.Index değerini değişiklik boyunca taşımadan sonraki eklemelerden sonra önceki katkıları geri alabilir.

Kaynaklar