Sla offsets op in een benoemde codering in plaats van oude stringindices te bewaren om tekst bij te houden die aan een Swift-string is toegevoegd. Deze handleiding bouwt een append-only UTF-16-segmentregister voor een kleine lokale tekstvoorbeeldberekening op een Mac.

De taak is specifiek: synthetische tekstfragmenten samenvoegen, onthouden waar elk fragment hoort en de exacte inhoud ervan terughalen nadat er later tekst is toegevoegd. Het voorbeeld werkt volledig in het geheugen en drukt de resultaten af. De segmentgrenzen geven aan welke bijdragen aan de tekst zijn geleverd; die kunnen afwijken van de grenzen tussen weergegeven tekens.

Waarom de oude eindindex opslaan onbetrouwbaar is

Een voor de hand liggende aanpak slaat text.endIndex op, voegt een fragment toe en combineert die opgeslagen index met de nieuwe text.endIndex. Daarmee wordt een index die vóór de mutatie is verkregen gecombineerd met een string van daarna. De documentatie van Swift over collecties waarschuwt expliciet dat muterende bewerkingen opgeslagen indices ongeldig kunnen maken. Een geslaagd resultaat met een kort voorbeeld biedt geen algemene garantie voor latere mutaties. Zie Apples documentatie over collecties.

Er is nog een probleem, zelfs als je de indices opnieuw opbouwt: door tekst toe te voegen kan de teken-grens op het samenvoegpunt veranderen. De taalgids van Swift laat dit zien met "cafe" gevolgd door "\u{0301}", een accent aigu als combining mark. De resulterende tekst bevat nog steeds vier Swift-Character-waarden, omdat het accent zich bij de voorgaande letter voegt. Zie Strings and Characters.

Daarom levert het vastleggen van het oude aantal tekens en het opschuiven met dat aantal tekens in de voltooide string niet betrouwbaar het begin van een toegevoegd fragment op. In het accentvoorbeeld begint de toegevoegde bijdrage binnen de uiteindelijke grafeemcluster. Er is geen afzonderlijk weergegeven teken dat met die bijdrage overeenkomt.

Behandel een toevoeggrens en een teken-grens als afzonderlijke concepten. De eerste legt vast hoe de tekst is opgebouwd; de tweede beschrijft hoe Swift de samengestelde tekst groepeert voor bewerkingen op tekenniveau.

Welke coördinaten moet het register opslaan?

Gebruik voor deze Mac-berekening gehele UTF-16-offsets en -lengtes. Apple documenteert dat NSString stringposities en -bereiken uitdrukt in UTF-16-code-eenheden. Swift’ String.utf16-weergave biedt bijbehorende code-eenheden, zodat het register één coördinatenstelsel kan gebruiken voor zowel metingen als substringextractie met Foundation. Zie NSString en de UTF-16-weergave van String.

Leestaak Geschikte representatie Voorwaarde
Elk oorspronkelijk fragment afzonderlijk terughalen Een array met eigen String-waarden Er zijn geen coördinaten in de samengevoegde tekst nodig
Exacte bijdragen in de samengevoegde tekst lokaliseren UTF-16-offsets en -lengtes Eerdere tekst blijft ongewijzigd
Werken met weergegeven tekens Verse tekenindices in de huidige string Segmentgrenzen moeten opnieuw als grafeemgrenzen worden beoordeeld
Posities bij willekeurige bewerkingen behouden Een bewerkingsbewust positiemodel Dit append-only-register volstaat niet

Het register hieronder heeft vier expliciete regels:

  1. Het begint met een lege string.
  2. De enige tekstmutatie is het toevoegen van een volledige Swift-String.
  3. Bij elke toevoeging worden het vorige UTF-16-totaal en de UTF-16-lengte van het fragment vastgelegd.
  4. Een segmentnummer heeft alleen betekenis in combinatie met de registerinstantie of momentopname die het heeft voortgebracht.

Door deze regels zijn de offsets bruikbaar. Het toevoegen van een achtervoegsel laat de eerder gecodeerde inhoud intact, zodat eerdere bereiken hun locatie behouden. Voor het invoegen, verwijderen, vervangen of transformeren van eerdere tekst is een ander contract nodig. Dit ontwerp is afgeleid van de gedocumenteerde stringrepresentaties; het is geen automatische functie van Swift voor positiebijhouding.

Bouw een volledig lokaal segmentregister

Gebruik een Swift-commandoregeldoel voor macOS met Foundation beschikbaar. Vervang het voorbeeldprogramma op hoofdniveau door het volledige programma hieronder. De implementatie gebruikt append(contentsOf:), waarvan de gedocumenteerde Swift-signatuur een andere String accepteert. Zie Apples referentie voor 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.")

Dit programma is gecontroleerd aan de hand van de opgehaalde API-documentatie; het is hier niet uitgevoerd omdat de beschikbare shell geen Swift-uitvoerbaar bestand bevat. De opgenomen precondities maken het verwachte gedrag van het voorbeeld expliciet wanneer je het op je Mac uitvoert.

Wat het voorbeeld moet controleren

De verwachte segmentcoördinaten zijn berekend op basis van de letterlijke invoerwaarden. Het combining accent levert één UTF-16-code-eenheid op, het lege fragment geen, en het blauwe vierkant twee. Het laatste achtervoegsel levert er vijf op. Dit zijn berekeningen op basis van de invoer ter illustratie, geen prestatiemetingen.

Segment Invoer UTF-16-locatie UTF-16-lengte
0 "cafe" 0 4
1 "\u{0301}" 4 1
2 "" 5 0
3 "\u{1F7E6}" 5 2
4 " done" 7 5

Het lege fragment krijgt een eigen segmentnummer, ook al deelt het een locatie met het volgende segment. Zo blijft de toevoeggeschiedenis behouden zonder tekst te verzinnen. Door expliciet "" terug te geven, wordt ook een substring-aanroep van Foundation voor dit geval vermeden.

De controle op het terughalen vergelijkt bewust UTF-16-arrays. Swift-stringvergelijking herkent canonieke equivalentie, wat betekent dat sommige strings met verschillende onderliggende scalairreeksen als gelijk worden beschouwd. Een assertie die de codering exact controleert, past beter bij het contract van dit register. Zie de bespreking van stringvergelijking in Strings and Characters.

Waarom de extractie Foundation gebruikt

De opgeslagen gehele getallen worden omgezet naar een NSRange en rechtstreeks doorgegeven aan NSString.substring(with:). Apple documenteert dat deze methode het opgegeven bereik extraheert en vereist dat dit bereik binnen de grenzen van de ontvanger blijft. Ongeldige bereiken kunnen een uitzondering veroorzaken. Het register beheert zowel de tekst als de bereiken en wijst onbekende segmentnummers af voordat de extractie plaatsvindt. Zie substring(with:).

Elke vastgelegde grens komt voort uit het toevoegen van een volledige Swift-string. Het voorbeeld maakt nooit een offset halverwege het UTF-16-surrogaatpaar van het blauwe vierkant. Een grens kan nog steeds binnen een samengestelde tekenreeks liggen, zoals het accent laat zien; het exact terughalen van bijdragen is juist het doel van deze representatie.

Gebruik String.count niet om deze Foundation-bereiken samen te stellen. Apple definieert NSString.length als het aantal UTF-16-code-eenheden; dat is een andere meting dan het aantal tekens en zegt niets over de weergegeven breedte. Zie NSString.length.

Houd mutatie, segmentaanmaak en momentopnamen bij elkaar

Voor een kleine synchrone berekening volstaat het waardetype: één eigenaar voegt fragmenten toe en leest het resultaat. De private setters voorkomen dat aanroepers de samengevoegde tekst los van de segmenttabel vervangen. Het gecachete UTF-16-totaal voorkomt ook dat bij elke toevoeging de groeiende samengevoegde string opnieuw wordt geteld.

Als meerdere asynchrone taken aan één register moeten bijdragen, plaats dat register dan achter één actor, of houd het op @MainActor wanneer het bij een Mac-interface hoort. Voer de toevoeging en de metadata-update samen uit in één synchrone geïsoleerde methode. Swift documenteert dat actor-geïsoleerd synchroon werk niet kan worden onderbroken bij een suspensiepunt binnen die methode. Zie Concurrency.

Geef de tekst en segmentmetadata samen terug als één waardemomentopname. Lees de tekst en de segmenttabel niet in afzonderlijke bewerkingen met await: tussen die aanroepen kan een andere toevoeging plaatsvinden. De Sendable-declaraties in het voorbeeld geven aan dat de waarden geschikt zijn om isolatiegrenzen over te steken; ze serialiseren geen gelijktijdige schrijfbewerkingen naar een gedeelde variabele. De documentatie over Sendable van Swift legt het verschil uit tussen overdraagbare waarden en afgeschermde veranderlijke toestand.

Als de volgorde van bijdragen van belang is, bepaal die dan voordat je ze toevoegt. Een actor beschermt de interne consistentie van het register; de toepassing moet nog steeds bepalen welk fragment eerst komt.

Weet wanneer deze offsets de taak niet meer kunnen uitvoeren

Doorloop deze diagnostische stappen voordat je een bereik opnieuw gebruikt:

  1. Controleer het eigenaarschap. Hoort het segmentnummer bij dit register of bij een compatibele, append-only afstammeling ervan?
  2. Controleer de mutatiegeschiedenis. Is er iets vóór het vastgelegde bereik gewijzigd? Bouw het register dan opnieuw op of kies een bewerkingsbewust model.
  3. Controleer de eenheden. Gebruikt de afnemer UTF-16-code-eenheden als coördinaten? Vervang die niet door aantallen tekens of UTF-8-bytes.
  4. Controleer het beoogde resultaat. Wil je de exact toegevoegde bijdrage terughalen of een volledig weergegeven teken selecteren? Daarvoor kunnen verschillende grenzen nodig zijn.

Een tekengeoriënteerde selectie kan soms niet alleen één toegevoegde bijdrage omvatten. Het accent hoort bij hetzelfde weergegeven teken als de voorafgaande letter. Als de desktoptaak markeren is, kies dan een expliciete presentatieregel: breid de selectie uit tot het omvattende teken of toon de bijdrage afzonderlijk in een segmentlijst. Bij uitbreiding wordt tekst uit een eerder segment meegenomen; behoud daarom de oorspronkelijke offsets voor exacte extractie.

Voor exact bijhouden van toevoegingen is de bruikbare invariant eenvoudig: houd het voorvoegsel ongewijzigd, benoem de coördinaateenheden en bewaar elk bereik bij de tekst ervan. Met dat contract kan het register eerdere bijdragen terughalen na latere toevoegingen, zonder één String.Index over een mutatie heen te bewaren.

Bronnen