Aby śledzić tekst dopisywany do ciągu Swift, zapisuj przesunięcia w określonym kodowaniu zamiast zachowywać stare indeksy ciągu. Ten przewodnik tworzy dopisywany wyłącznie na końcu rejestr fragmentów UTF-16 na potrzeby niewielkiego lokalnego obliczenia podglądu tekstu na Macu.
Zadanie jest konkretne: połączyć syntetyczne fragmenty tekstu, zapamiętać miejsce każdego z nich i odzyskać ich dokładną zawartość po kolejnych dopisaniach. Przykład działa w całości w pamięci i wyświetla wyniki. Granice segmentów opisują wkład poszczególnych fragmentów w tekst, co może różnić się od granic między wyświetlanymi znakami.
Dlaczego zapisywanie starego indeksu końca jest zawodne
Kuszące podejście polega na zapisaniu text.endIndex, dopisaniu fragmentu i połączeniu tego zapisanego indeksu z nowym text.endIndex. W ten sposób miesza się indeks uzyskany przed mutacją z ciągiem uzyskanym po niej. Dokumentacja kolekcji Swift wyraźnie ostrzega, że operacje mutujące mogą unieważnić zapisane indeksy. Pomyślny wynik dla krótkiej próbki nie ustanawia ogólnej gwarancji dotyczącej późniejszych mutacji. Zobacz dokumentację Apple dotyczącą kolekcji.
Nawet jeśli odtworzysz indeksy, pojawia się drugi problem: dopisanie tekstu może zmienić granicę znaku w miejscu połączenia. W przewodniku języka Swift pokazano to na przykładzie "cafe" połączonego z "\u{0301}", czyli łączącym akcentem ostrym. Powstały tekst nadal zawiera cztery wartości typu Character w Swift, ponieważ akcent łączy się z poprzedzającą literą. Zobacz Ciągi i znaki.
W związku z tym zapisanie wcześniejszej liczby znaków i przesunięcie się o tyle znaków w gotowym ciągu nie pozwala niezawodnie odzyskać początku dopisanego fragmentu. W przykładzie z akcentem dopisywany fragment zaczyna się wewnątrz końcowego klastra grafemowego. Nie ma osobnego wyświetlanego znaku odpowiadającego temu fragmentowi.
Traktuj granicę dopisania i granicę znaku jako odrębne pojęcia. Pierwsza zapisuje sposób złożenia tekstu, a druga opisuje sposób grupowania złożonego tekstu przez Swift na potrzeby operacji na znakach.
Jakie współrzędne powinien przechowywać rejestr?
Na potrzeby tego obliczenia na Macu użyj całkowitych przesunięć i długości w UTF-16. Apple dokumentuje, że NSString wyraża pozycje i zakresy ciągu w jednostkach kodowych UTF-16. Widok String.utf16 w Swift udostępnia odpowiadające im jednostki kodowe, więc rejestr może używać jednego układu współrzędnych zarówno do pomiaru, jak i do wyodrębniania podciągów przez Foundation. Zobacz NSString i widok UTF-16 typu String.
| Zadanie czytelnika | Odpowiednia reprezentacja | Warunek |
|---|---|---|
| Niezależne odzyskanie każdego oryginalnego fragmentu | Tablica posiadanych wartości String |
Nie są potrzebne współrzędne w połączonym tekście |
| Wskazanie dokładnych fragmentów w połączonym tekście | Przesunięcia i długości w UTF-16 | Wcześniejszy tekst pozostaje niezmieniony |
| Praca z wyświetlanymi znakami | Nowe indeksy znaków w bieżącym ciągu | Granice segmentów trzeba ponownie rozpatrzyć jako granice klastrów grafemowych |
| Zachowanie pozycji podczas dowolnych edycji | Model pozycji uwzględniający edycje | Ten rejestr dopisywany wyłącznie na końcu nie wystarcza |
Poniższy rejestr ma cztery jawne reguły:
- Zaczyna się od pustego ciągu.
- Jedyną mutacją tekstu jest dopisanie kompletnego ciągu Swift
String. - Każde dopisanie zapisuje poprzednią sumę UTF-16 oraz długość fragmentu w UTF-16.
- Numer segmentu ma znaczenie wyłącznie w połączeniu z instancją rejestru lub migawką, która go utworzyła.
Te reguły sprawiają, że przesunięcia są użyteczne. Dopisanie sufiksu pozostawia wcześniejszą zakodowaną zawartość na miejscu, więc poprzednie zakresy zachowują swoje położenie. Wstawianie, usuwanie, zastępowanie lub przekształcanie wcześniejszego tekstu wymagałoby innej gwarancji. Jest to projekt wynikający z udokumentowanych reprezentacji ciągów, a nie automatyczna funkcja śledzenia pozycji zapewniana przez Swift.
Utwórz kompletny lokalny rejestr segmentów
Użyj docelowego programu wiersza poleceń Swift dla macOS z dostępnym Foundation. Zastąp przykładowy program najwyższego poziomu poniższym kompletnym programem. Implementacja używa append(contentsOf:), którego udokumentowana sygnatura w Swift przyjmuje inny String. Zobacz dokumentację Apple dotyczącą append(contentsOf:).
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.")
Ten program sprawdzono na podstawie pobranej dokumentacji API; nie został tutaj uruchomiony, ponieważ w dostępnej powłoce nie ma pliku wykonywalnego Swift. Zawarte warunki wstępne jasno określają oczekiwane zachowanie przykładu, gdy uruchomisz go na Macu.
Co ma sprawdzać przykład
Oczekiwane współrzędne segmentów obliczono na podstawie literałów wejściowych. Łączący akcent wnosi jedną jednostkę kodową UTF-16, pusty fragment zero, a niebieski kwadrat dwie. Końcowy sufiks wnosi pięć. Są to przykładowe obliczenia dla danych wejściowych, a nie pomiary wydajności.
| Segment | Wejście | Położenie w UTF-16 | Długość w UTF-16 |
|---|---|---|---|
| 0 | "cafe" |
0 | 4 |
| 1 | "\u{0301}" |
4 | 1 |
| 2 | "" |
5 | 0 |
| 3 | "\u{1F7E6}" |
5 | 2 |
| 4 | " done" |
7 | 5 |
Pusty fragment otrzymuje własny numer segmentu, mimo że ma takie samo położenie jak następny segment. Pozwala to zachować historię dopisań bez wymyślania tekstu. Jawny zwrot "" pozwala też uniknąć w tym przypadku wywołania metody Foundation do wyodrębniania podciągu.
Kontrola odzyskiwania celowo porównuje tablice UTF-16. Równość ciągów w Swift uwzględnia równoważność kanoniczną, co oznacza, że niektóre ciągi o różnych bazowych sekwencjach skalarów są uznawane za równe. Asercja dokładna względem kodowania lepiej pasuje do założeń tego rejestru. Zobacz omówienie porównywania ciągów w sekcji Ciągi i znaki.
Dlaczego wyodrębnianie używa Foundation
Zapisane liczby są przekształcane w NSRange i przekazywane bezpośrednio do NSString.substring(with:). Apple dokumentuje, że ta metoda wyodrębnia wskazany zakres i wymaga, aby mieścił się on w granicach odbiorcy. Nieprawidłowe zakresy mogą spowodować wyjątek. Rejestr kontroluje zarówno tekst, jak i jego zakresy, a przed wyodrębnieniem odrzuca nieznane numery segmentów. Zobacz substring(with:).
Każda zapisana granica pochodzi z dopisania kompletnego ciągu Swift. Przykład nigdy nie tworzy przesunięcia w połowie pary zastępczej UTF-16 niebieskiego kwadratu. Granica nadal może wypadać wewnątrz złożonej sekwencji znaków, jak pokazuje przykład z akcentem; celem tej reprezentacji jest odzyskiwanie dokładnych fragmentów.
Nie używaj String.count do tworzenia tych zakresów Foundation. Apple definiuje NSString.length jako liczbę jednostek kodowych UTF-16, która jest inną miarą niż liczba znaków i nie opisuje szerokości renderowanego tekstu. Zobacz NSString.length.
Łącz mutację, tworzenie segmentów i migawki
W przypadku niewielkiego obliczenia synchronicznego wystarczy typ wartości: jeden właściciel dopisuje fragmenty i odczytuje wynik. Jego prywatne settery uniemożliwiają zastępowanie połączonego tekstu niezależnie od tabeli segmentów. Buforowana suma UTF-16 pozwala też uniknąć ponownego zliczania rosnącego połączonego ciągu przy każdym dopisaniu.
Jeśli kilka zadań asynchronicznych musi współtworzyć jeden rejestr, umieść go za pojedynczym aktorem albo trzymaj go na @MainActor, gdy należy do interfejsu Maca. Wykonuj dopisanie i aktualizację metadanych w jednej synchronicznej metodzie izolowanej. Swift dokumentuje, że synchroniczna praca izolowana przez aktora nie może zostać przerwana w punkcie zawieszenia wewnątrz tej metody. Zobacz Współbieżność.
Zwracaj tekst i metadane segmentów razem jako jedną migawkę wartości. Unikaj osobnego oczekiwania na odczyt tekstu i odczyt tabeli segmentów: między tymi wywołaniami może nastąpić kolejne dopisanie. Deklaracje Sendable w przykładzie opisują wartości, które można przenosić między granicami izolacji; nie serializują równoczesnych zapisów do współdzielonej zmiennej. Dokumentacja Swift dotycząca Sendable wyjaśnia różnicę między przenośnymi wartościami a chronionym zmiennym stanem.
Jeśli kolejność fragmentów ma znaczenie, ustal ją przed dopisywaniem. Aktor chroni wewnętrzną spójność rejestru, a aplikacja nadal musi zdecydować, który fragment pojawi się pierwszy.
Wiedz, kiedy te przesunięcia przestają odpowiadać zadaniu
Zanim ponownie użyjesz zakresu, przejdź tę sekwencję diagnostyczną:
- Sprawdź właściciela. Czy numer segmentu należy do tego rejestru lub jednego z jego zgodnych potomków dopisywanych wyłącznie na końcu?
- Sprawdź historię mutacji. Czy cokolwiek zmieniło się przed zapisanym zakresem? Jeśli tak, odbuduj rejestr albo zastosuj model uwzględniający edycje.
- Sprawdź jednostki. Czy współrzędne odbiorcy to jednostki kodowe UTF-16? Nie zastępuj ich liczbą znaków ani bajtów UTF-8.
- Sprawdź oczekiwany wynik. Czy odzyskujesz dokładnie dopisany fragment, czy wybierasz cały wyświetlany znak? Mogą być potrzebne różne granice.
Wybór oparty na znakach nie zawsze pozwala wskazać wyłącznie jeden dopisany fragment. Akcent należy do tego samego wyświetlanego znaku co poprzedzająca go litera. Jeśli zadanie na komputerze polega na podświetlaniu, wybierz jawną regułę prezentacji: rozszerz zaznaczenie do obejmującego go znaku albo pokaż fragment osobno na liście segmentów. Rozszerzenie obejmuje tekst z wcześniejszego segmentu, więc zachowaj pierwotne przesunięcia do dokładnego odzyskania.
Przy dokładnym śledzeniu dopisań obowiązuje prosta niezmienność: zachowuj niezmieniony prefiks, określaj jednostki współrzędnych i przechowuj każdy zakres wraz z jego tekstem. Przy takich założeniach rejestr może odzyskać wcześniejsze fragmenty po kolejnych dopisaniach bez przenoszenia pojedynczego String.Index przez mutację.
Źródła
Kolekcja — Apple.
Ciągi i znaki — Swift.org.
String — Apple.
NSString — Apple.
append(contentsOf:) — Apple.
substring(with:) — Apple.
length — Apple.
Współbieżność — Swift.org.
Sendable — Swift.org.