Untuk melacak teks yang ditambahkan ke string Swift, simpan offset dalam pengodean yang ditentukan namanya, alih-alih mempertahankan indeks string lama. Panduan ini membuat ledger segmen UTF-16 yang hanya dapat ditambahi untuk perhitungan pratinjau teks lokal sederhana di Mac.
Tugasnya spesifik: gabungkan fragmen teks sintetis, ingat lokasi setiap fragmen, dan pulihkan konten persisnya setelah penambahan berikutnya. Contoh ini sepenuhnya berjalan di memori dan mencetak hasilnya. Batas segmennya menunjukkan kontribusi terhadap teks, yang dapat berbeda dari batas antara karakter yang ditampilkan.
Mengapa menyimpan indeks akhir yang lama tidak dapat diandalkan
Pendekatan yang menggoda adalah menyimpan text.endIndex, menambahkan fragmen, lalu memasangkan indeks yang tersimpan itu dengan text.endIndex yang baru. Cara ini mencampurkan indeks yang diperoleh sebelum mutasi dengan string setelah mutasi. Dokumentasi koleksi Swift secara eksplisit memperingatkan bahwa operasi mutasi dapat membatalkan indeks yang tersimpan. Hasil yang berhasil pada sampel pendek tidak membuktikan adanya jaminan umum untuk mutasi berikutnya. Lihat dokumentasi Collection dari Apple.
Ada masalah kedua, bahkan jika Anda membuat ulang indeks: penambahan teks dapat mengubah batas karakter di titik sambungan. Panduan bahasa Swift menunjukkan hal ini dengan "cafe" yang diikuti "\u{0301}", yaitu aksen akut penggabung. Teks hasilnya tetap berisi empat nilai Character Swift karena aksen tersebut bergabung dengan huruf sebelumnya. Lihat Strings and Characters.
Akibatnya, mencatat jumlah karakter lama lalu maju sebanyak itu dalam string yang sudah selesai tidak dapat diandalkan untuk memulihkan awal fragmen yang ditambahkan. Dalam contoh aksen tersebut, kontribusi yang ditambahkan dimulai di dalam gugus grafem terakhir. Tidak ada karakter tampilan terpisah yang mewakili kontribusi itu.
Perlakukan batas penambahan dan batas karakter sebagai dua konsep yang berbeda. Yang pertama mencatat cara teks dirangkai; yang kedua menjelaskan cara Swift mengelompokkan teks hasil rangkaian untuk operasi tingkat karakter.
Koordinat apa yang sebaiknya disimpan ledger?
Untuk perhitungan Mac ini, gunakan offset dan panjang integer UTF-16. Apple mendokumentasikan bahwa posisi dan rentang string NSString dinyatakan dalam unit kode UTF-16. Tampilan String.utf16 di Swift menyediakan unit kode yang sesuai, sehingga ledger dapat menggunakan satu sistem koordinat untuk pengukuran dan ekstraksi substring Foundation. Lihat NSString dan tampilan UTF-16 String.
| Tugas pembaca | Representasi yang sesuai | Kondisi |
|---|---|---|
| Pulihkan setiap fragmen asli secara terpisah | Larik nilai String yang dimiliki |
Koordinat dalam teks gabungan tidak diperlukan |
| Temukan kontribusi persis dalam teks gabungan | Offset dan panjang UTF-16 | Teks sebelumnya tetap tidak berubah |
| Bekerja dengan karakter yang ditampilkan | Indeks karakter baru dalam string saat ini | Batas segmen harus ditinjau ulang sebagai batas gugus grafem |
| Pertahankan posisi melalui perubahan arbitrer | Model posisi yang menyadari perubahan | Ledger yang hanya dapat ditambahi ini tidak memadai |
Ledger di bawah ini memiliki empat aturan eksplisit:
- Dimulai dengan string kosong.
- Satu-satunya mutasi teksnya adalah menambahkan satu
StringSwift secara utuh. - Setiap penambahan mencatat total UTF-16 sebelumnya dan panjang UTF-16 fragmen.
- Nomor segmen hanya bermakna jika disertai instans atau snapshot ledger yang menghasilkannya.
Aturan ini membuat offset berguna. Penambahan akhiran mempertahankan konten terkode sebelumnya, sehingga rentang terdahulu tetap berada di lokasi yang sama. Penyisipan, penghapusan, penggantian, atau transformasi teks sebelumnya memerlukan kontrak yang berbeda. Ini adalah rancangan yang diturunkan dari representasi string yang didokumentasikan, bukan fitur pelacakan posisi otomatis yang disediakan Swift.
Buat ledger segmen lokal yang lengkap
Gunakan target baris perintah Swift macOS yang menyediakan Foundation. Ganti program tingkat atas sampelnya dengan program lengkap di bawah ini. Implementasi menggunakan append(contentsOf:), yang tanda tangan Swift-nya terdokumentasi menerima String lain. Lihat referensi append(contentsOf:) dari Apple.
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.")
Program ini telah diperiksa berdasarkan dokumentasi API yang diperoleh; program tidak dijalankan di sini karena shell yang tersedia tidak memiliki executable Swift. Prasyarat yang disertakan membuat perilaku sampel yang diharapkan menjadi jelas saat Anda menjalankannya di Mac.
Hal yang dirancang untuk diperiksa oleh sampel
Koordinat segmen yang diharapkan dihitung dari masukan literal. Aksen penggabung menyumbang satu unit kode UTF-16, fragmen kosong menyumbang nol, dan kotak biru menyumbang dua. Akhiran terakhir menyumbang lima. Ini adalah perhitungan ilustratif dari masukan, bukan pengukuran kinerja.
| Segmen | Masukan | Lokasi UTF-16 | Panjang UTF-16 |
|---|---|---|---|
| 0 | "cafe" |
0 | 4 |
| 1 | "\u{0301}" |
4 | 1 |
| 2 | "" |
5 | 0 |
| 3 | "\u{1F7E6}" |
5 | 2 |
| 4 | " done" |
7 | 5 |
Fragmen kosong mendapat nomor segmennya sendiri meskipun lokasinya sama dengan segmen berikutnya. Ini mempertahankan riwayat penambahan tanpa mengada-adakan teks. Mengembalikan "" secara eksplisit juga menghindari pemanggilan substring Foundation untuk kasus ini.
Pemeriksaan pemulihan sengaja membandingkan larik UTF-16. Kesetaraan string Swift mengenali kesetaraan kanonis, yang berarti beberapa string dengan urutan skalar dasar berbeda dapat dianggap sama. Pernyataan encoding yang tepat lebih sesuai dengan kontrak ledger ini. Lihat pembahasan perbandingan string dalam Strings and Characters.
Mengapa ekstraksi menggunakan Foundation
Integer yang disimpan diubah menjadi NSRange dan langsung diberikan ke NSString.substring(with:). Apple mendokumentasikan bahwa metode ini mengekstrak rentang yang ditentukan dan mensyaratkan rentang tersebut berada dalam batas penerima. Rentang yang tidak valid dapat menyebabkan exception. Ledger mengendalikan teks dan rentangnya, serta menolak nomor segmen yang tidak dikenal sebelum ekstraksi. Lihat substring(with:).
Setiap batas yang dicatat berasal dari penambahan string Swift secara utuh. Contoh ini tidak pernah membuat offset di tengah pasangan surrogate UTF-16 kotak biru. Batas tetap dapat berada di dalam urutan karakter gabungan, seperti yang ditunjukkan oleh aksen; memulihkan kontribusi secara persis adalah tujuan representasi ini.
Jangan gunakan String.count untuk membentuk rentang Foundation ini. Apple mendefinisikan NSString.length sebagai jumlah unit kode UTF-16, ukuran yang berbeda dari jumlah karakter dan tidak menjelaskan lebar tampilan. Lihat NSString.length.
Satukan mutasi, pembuatan segmen, dan snapshot
Untuk perhitungan sinkron sederhana, tipe nilai sudah memadai: satu pemilik menambahkan fragmen dan membaca hasilnya. Setter privatnya mencegah pemanggil mengganti teks gabungan secara terpisah dari tabel segmen. Total UTF-16 yang disimpan dalam cache juga menghindari penghitungan ulang string gabungan yang terus membesar pada setiap penambahan.
Jika beberapa tugas asinkron harus berkontribusi pada satu ledger, tempatkan ledger itu di balik satu actor, atau pertahankan di @MainActor jika ledger tersebut merupakan bagian dari antarmuka Mac. Lakukan penambahan dan pembaruan metadata dalam satu metode sinkron yang terisolasi. Swift mendokumentasikan bahwa pekerjaan sinkron yang terisolasi actor tidak dapat disela pada titik suspensi di dalam metode tersebut. Lihat Concurrency.
Kembalikan teks dan metadata segmen bersama-sama sebagai satu nilai snapshot. Hindari membaca teks dan tabel segmen lewat operasi await terpisah: penambahan lain dapat terjadi di antara kedua pemanggilan itu. Deklarasi Sendable pada contoh menjelaskan nilai yang cocok untuk melintasi batas isolasi; deklarasi itu tidak mengurutkan penulisan serentak ke variabel bersama. Dokumentasi Sendable dari Swift menjelaskan perbedaan antara nilai yang dapat ditransfer dan status mutable yang dilindungi.
Jika urutan kontribusi penting, tentukan urutannya sebelum menambahkan. Actor melindungi konsistensi internal ledger, sementara aplikasi tetap perlu menentukan fragmen mana yang lebih dulu.
Ketahui kapan offset ini tidak lagi menjawab kebutuhan
Gunakan urutan pemeriksaan berikut sebelum menggunakan kembali rentang:
- Periksa kepemilikan. Apakah nomor segmen ini milik ledger tersebut atau salah satu turunannya yang kompatibel dan hanya dapat ditambahi?
- Periksa riwayat mutasi. Apakah ada sesuatu yang berubah sebelum rentang yang dicatat? Jika ya, buat ulang ledger atau gunakan model yang menyadari perubahan.
- Periksa satuan. Apakah koordinat konsumen berupa unit kode UTF-16? Jangan menggantinya dengan jumlah karakter atau jumlah byte UTF-8.
- Periksa hasil yang dimaksud. Apakah Anda memulihkan kontribusi yang persis ditambahkan, atau memilih satu karakter utuh yang ditampilkan? Keduanya dapat memerlukan batas yang berbeda.
Pemilihan yang berorientasi karakter tidak selalu dapat merepresentasikan hanya satu kontribusi yang ditambahkan. Aksen tersebut merupakan bagian dari karakter tampilan yang sama dengan huruf sebelumnya. Jika tugas desktopnya adalah penyorotan, tentukan aturan penyajian secara eksplisit: perluas pilihan hingga mencakup karakter yang berisi kontribusi tersebut, atau tampilkan kontribusi secara terpisah dalam daftar segmen. Perluasan akan menyertakan teks dari segmen sebelumnya, jadi pertahankan offset asli untuk pemulihan yang persis.
Untuk pelacakan penambahan yang persis, invarian yang berguna sederhana: pertahankan prefiks tanpa perubahan, tentukan satuan koordinat, dan simpan setiap rentang bersama teksnya. Dengan kontrak itu, ledger dapat memulihkan kontribusi terdahulu setelah penambahan berikutnya tanpa membawa satu String.Index melintasi mutasi.
Sumber
Collection — Apple.
Strings and Characters — Swift.org.
String — Apple.
NSString — Apple.
append(contentsOf:) — Apple.
substring(with:) — Apple.
length — Apple.
Concurrency — Swift.org.
Sendable — Swift.org.