Per tenere traccia del testo aggiunto a una stringa Swift, memorizza gli offset in una codifica definita, invece di conservare vecchi indici della stringa. Questa guida crea un registro UTF-16 di segmenti in sola aggiunta per un piccolo calcolo locale di anteprima del testo su Mac.
Il compito è specifico: unire frammenti di testo sintetici, ricordare dove si trova ciascun frammento e recuperarne il contenuto esatto dopo aggiunte successive. L’esempio opera interamente in memoria e stampa i risultati. I confini dei segmenti descrivono i contributi al testo, che possono differire dai confini tra i caratteri visualizzati.
Perché salvare il vecchio indice finale non è affidabile
Un approccio allettante salva text.endIndex, aggiunge un frammento e abbina l’indice salvato al nuovo text.endIndex. Così si mescolano un indice ottenuto prima della modifica e una stringa ottenuta dopo. La documentazione delle collezioni di Swift avverte esplicitamente che le operazioni di modifica possono invalidare gli indici salvati. Un risultato corretto con un breve esempio non dimostra che il comportamento sia garantito per modifiche successive. Vedi la documentazione di Collection di Apple.
C’è un secondo problema anche se ricostruisci gli indici: aggiungere testo può cambiare il confine tra caratteri nel punto di giunzione. La guida al linguaggio Swift lo mostra con "cafe" seguito da "\u{0301}", un accento acuto combinante. Il testo risultante contiene ancora quattro valori Character di Swift, perché l’accento si unisce alla lettera precedente. Vedi Stringhe e caratteri.
Di conseguenza, registrare il vecchio numero di caratteri e avanzare di altrettanti caratteri nella stringa completata non permette di recuperare in modo affidabile l’inizio di un frammento aggiunto. Nell’esempio dell’accento, il contributo aggiunto inizia all’interno del cluster di grafemi finale. Non esiste un carattere visualizzato separato che corrisponda a quel contributo.
Considera il confine di un’aggiunta e il confine di un carattere come concetti distinti. Il primo registra come è stato assemblato il testo; il secondo descrive come Swift raggruppa il testo assemblato per le operazioni a livello di carattere.
Quali coordinate dovrebbe memorizzare il registro?
Per questo calcolo su Mac, usa offset e lunghezze interi UTF-16. Apple documenta che NSString esprime le posizioni e gli intervalli delle stringhe in unità di codice UTF-16. La vista String.utf16 di Swift espone le stesse unità di codice, quindi il registro può usare un unico sistema di coordinate sia per la misurazione sia per estrarre sottostringhe con Foundation. Vedi NSString e la vista UTF-16 di String.
| Esigenza di chi legge | Rappresentazione adatta | Condizione |
|---|---|---|
| Recuperare ogni frammento originale separatamente | Un array di valori String posseduti |
Non servono coordinate nel testo combinato |
| Individuare i contributi esatti nel testo combinato | Offset e lunghezze UTF-16 | Il testo precedente rimane invariato |
| Lavorare con i caratteri visualizzati | Indici di carattere aggiornati nella stringa corrente | I confini dei segmenti devono essere rivalutati come confini di grafemi |
| Mantenere le posizioni durante modifiche arbitrarie | Un modello di posizioni consapevole delle modifiche | Questo registro in sola aggiunta non è sufficiente |
Il registro seguente ha quattro regole esplicite:
- Inizia con una stringa vuota.
- L’unica modifica al testo consiste nell’aggiungere una
StringSwift completa. - Ogni aggiunta registra il totale UTF-16 precedente e la lunghezza UTF-16 del frammento.
- Un numero di segmento ha significato solo insieme all’istanza del registro o allo snapshot che lo ha prodotto.
Queste regole rendono utili gli offset. L’aggiunta di un suffisso lascia intatto il contenuto codificato precedente, quindi gli intervalli già registrati mantengono le loro posizioni. Inserire, rimuovere, sostituire o trasformare il testo precedente richiederebbe un contratto diverso. Si tratta di una progettazione derivata dalle rappresentazioni delle stringhe documentate, non di una funzionalità automatica di Swift per tenere traccia delle posizioni.
Crea un registro completo dei segmenti locale
Usa un target a riga di comando Swift per macOS con Foundation disponibile. Sostituisci il programma di esempio al livello superiore con il programma completo qui sotto. L’implementazione usa append(contentsOf:), la cui firma Swift documentata accetta un’altra String. Vedi il riferimento di Apple a 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.")
Questo programma è stato confrontato con la documentazione API recuperata; qui non è stato eseguito perché la shell disponibile non include un eseguibile Swift. Le precondizioni incluse rendono esplicito il comportamento atteso dell’esempio quando lo esegui sul tuo Mac.
Cosa è progettato per verificare l’esempio
Le coordinate attese dei segmenti sono calcolate a partire dagli input letterali. L’accento combinante contribuisce con un’unità di codice UTF-16, il frammento vuoto ne contribuisce zero e il quadrato blu ne contribuisce due. Il suffisso finale ne contribuisce cinque. Sono calcoli illustrativi sugli input, non misurazioni delle prestazioni.
| Segmento | Input | Posizione UTF-16 | Lunghezza UTF-16 |
|---|---|---|---|
| 0 | "cafe" |
0 | 4 |
| 1 | "\u{0301}" |
4 | 1 |
| 2 | "" |
5 | 0 |
| 3 | "\u{1F7E6}" |
5 | 2 |
| 4 | " done" |
7 | 5 |
Al frammento vuoto viene assegnato un numero di segmento proprio, anche se condivide la posizione con il segmento successivo. In questo modo si conserva la cronologia delle aggiunte senza inventare testo. Restituire esplicitamente "" evita anche di chiamare la sottostringa di Foundation in questo caso.
Il controllo del recupero confronta deliberatamente gli array UTF-16. L’uguaglianza tra stringhe di Swift riconosce l’equivalenza canonica: alcune stringhe con sequenze scalari sottostanti diverse risultano quindi uguali. Un’asserzione che confronti esattamente la codifica è più adatta al contratto di questo registro. Vedi la trattazione del confronto tra stringhe in Stringhe e caratteri.
Perché l’estrazione usa Foundation
Gli interi memorizzati vengono convertiti in un NSRange e passati direttamente a NSString.substring(with:). Apple documenta che questo metodo estrae l’intervallo specificato e richiede che resti entro i limiti del destinatario. Gli intervalli non validi possono causare un’eccezione. Il registro gestisce sia il testo sia i suoi intervalli e rifiuta i numeri di segmento sconosciuti prima dell’estrazione. Vedi substring(with:).
Ogni confine registrato deriva dall’aggiunta di una stringa Swift completa. L’esempio non crea mai un offset a metà della coppia surrogata UTF-16 del quadrato blu. Un confine può comunque trovarsi all’interno di una sequenza di caratteri composta, come mostra l’accento: questa rappresentazione serve proprio a recuperare i contributi esatti.
Non usare String.count per creare questi intervalli Foundation. Apple definisce NSString.length come il conteggio delle unità di codice UTF-16: una misura diversa dal numero di caratteri, che non descrive la larghezza visualizzata. Vedi NSString.length.
Mantieni insieme modifica, creazione dei segmenti e snapshot
Per un piccolo calcolo sincrono è sufficiente il tipo valore: un proprietario aggiunge frammenti e legge il risultato. I suoi setter privati impediscono ai chiamanti di sostituire il testo combinato senza aggiornare la tabella dei segmenti. Il totale UTF-16 memorizzato nella cache evita inoltre di ricalcolare la lunghezza della stringa combinata crescente a ogni aggiunta.
Se più attività asincrone devono contribuire a un unico registro, racchiudilo in un solo actor oppure mantienilo su @MainActor se appartiene a un’interfaccia Mac. Esegui l’aggiunta e l’aggiornamento dei metadati insieme in un unico metodo sincrono isolato. Swift documenta che il lavoro sincrono isolato da un actor non può essere interrotto in un punto di sospensione all’interno di quel metodo. Vedi Concorrenza.
Restituisci insieme testo e metadati dei segmenti come un unico snapshot di valore. Evita di attendere separatamente la lettura del testo e quella della tabella dei segmenti: tra le due chiamate potrebbe verificarsi un’altra aggiunta. Le dichiarazioni Sendable dell’esempio descrivono valori adatti ad attraversare i confini di isolamento; non serializzano le scritture concorrenti su una variabile condivisa. La documentazione di Sendable di Swift spiega la distinzione tra valori trasferibili e stato mutabile protetto.
Se l’ordine dei contributi è importante, stabiliscilo prima di aggiungerli. Un actor protegge la coerenza interna del registro, mentre l’applicazione deve comunque decidere quale frammento viene prima.
Scopri quando questi offset non rispondono più al compito
Prima di riutilizzare un intervallo, segui questa sequenza diagnostica:
- Controlla la proprietà. Il numero di segmento appartiene a questo registro o a uno dei suoi discendenti compatibili in sola aggiunta?
- Controlla la cronologia delle modifiche. È cambiato qualcosa prima dell’intervallo registrato? In tal caso, ricrea il registro o adotta un modello consapevole delle modifiche.
- Controlla le unità. Le coordinate usate dal consumatore sono unità di codice UTF-16? Non sostituirle con conteggi di caratteri o byte UTF-8.
- Controlla il risultato desiderato. Vuoi recuperare il contributo aggiunto esatto o selezionare un intero carattere visualizzato? Potrebbero richiedere confini diversi.
Una selezione orientata ai caratteri non può sempre rappresentare un solo contributo aggiunto. L’accento appartiene allo stesso carattere visualizzato della lettera precedente. Se l’attività su desktop consiste nell’evidenziare, scegli una regola di presentazione esplicita: estendi la selezione fino a includere il carattere che lo contiene oppure mostra il contributo separatamente in un elenco di segmenti. L’estensione include testo di un segmento precedente, quindi conserva gli offset originali per il recupero esatto.
Per tenere traccia esattamente delle aggiunte, l’invariante utile è semplice: mantieni invariato il prefisso, specifica le unità delle coordinate e conserva ogni intervallo insieme al suo testo. Con questo contratto, il registro può recuperare i contributi precedenti dopo aggiunte successive senza portarsi dietro un singolo String.Index attraverso una modifica.
Fonti
Collection — Apple.
Stringhe e caratteri — Swift.org.
String — Apple.
NSString — Apple.
append(contentsOf:) — Apple.
substring(with:) — Apple.
length — Apple.
Concorrenza — Swift.org.
Sendable — Swift.org.