Pour suivre le texte ajouté à une chaîne Swift, enregistrez les décalages dans un encodage nommé au lieu de conserver d’anciens indices de chaîne. Ce guide crée un registre de segments UTF-16 en ajout uniquement pour un petit calcul local d’aperçu de texte sur Mac.
La tâche est précise : assembler des fragments de texte synthétiques, mémoriser l’emplacement de chaque fragment et en récupérer le contenu exact après des ajouts ultérieurs. L’exemple s’exécute entièrement en mémoire et affiche ses résultats. Les limites de ses segments décrivent les contributions au texte, qui peuvent différer des limites entre les caractères affichés.
Pourquoi enregistrer l’ancien indice de fin n’est pas fiable
Une approche tentante consiste à enregistrer text.endIndex, à ajouter un fragment, puis à associer cet indice enregistré au nouveau text.endIndex. On mélange ainsi un indice obtenu avant la modification avec une chaîne obtenue après celle-ci. La documentation de Swift sur les collections avertit explicitement que les opérations de mutation peuvent invalider les indices enregistrés. Un résultat correct avec un court exemple ne garantit pas un comportement général lors de mutations ultérieures. Consultez la documentation sur les collections d’Apple.
Il existe un second problème, même si vous reconstruisez les indices : l’ajout de texte peut modifier la limite entre les caractères à la jonction. Le guide du langage Swift l’illustre avec "cafe" suivi de "\u{0301}", un accent aigu combinant. Le texte obtenu contient toujours quatre valeurs Character Swift, car l’accent se combine avec la lettre précédente. Consultez Chaînes et caractères.
Par conséquent, enregistrer l’ancien nombre de caractères puis avancer d’autant de caractères dans la chaîne terminée ne permet pas de retrouver de manière fiable le début d’un fragment ajouté. Dans l’exemple avec l’accent, la contribution ajoutée commence à l’intérieur du groupe de graphèmes final. Aucun caractère affiché distinct ne correspond à cette contribution.
Traitez une limite d’ajout et une limite de caractère comme deux notions distinctes. La première indique comment le texte a été assemblé ; la seconde décrit la manière dont Swift regroupe le texte assemblé pour les opérations au niveau des caractères.
Quelles coordonnées le registre doit-il stocker ?
Pour ce calcul sur Mac, utilisez des décalages et des longueurs entiers en UTF-16. Apple indique que NSString exprime les positions et les plages de ses chaînes en unités de code UTF-16. La vue String.utf16 de Swift expose les mêmes unités de code ; le registre peut donc utiliser un seul système de coordonnées pour la mesure et l’extraction de sous-chaînes avec Foundation. Consultez NSString et la vue UTF-16 de String.
| Tâche du lecteur | Représentation appropriée | Condition |
|---|---|---|
| Récupérer séparément chaque fragment d’origine | Un tableau de valeurs String possédées |
Aucune coordonnée dans le texte assemblé n’est nécessaire |
| Localiser les contributions exactes dans le texte assemblé | Décalages et longueurs UTF-16 | Le texte antérieur reste inchangé |
| Travailler avec les caractères affichés | Nouveaux indices de caractères dans la chaîne actuelle | Les limites des segments doivent être réévaluées comme limites de graphèmes |
| Préserver les positions malgré des modifications arbitraires | Un modèle de positions tenant compte des modifications | Ce registre en ajout uniquement ne suffit pas |
Le registre ci-dessous suit quatre règles explicites :
- Il commence par une chaîne vide.
- Sa seule modification de texte consiste à ajouter une valeur
StringSwift complète. - Chaque ajout enregistre le total UTF-16 précédent et la longueur UTF-16 du fragment.
- Un numéro de segment n’a de sens qu’avec l’instance du registre ou l’instantané qui l’a produit.
Ces règles rendent les décalages utiles. L’ajout d’un suffixe laisse le contenu encodé précédent en place, de sorte que les plages antérieures conservent leur emplacement. L’insertion, la suppression, le remplacement ou la transformation de texte antérieur nécessiterait un contrat différent. Il s’agit d’une conception déduite des représentations de chaînes documentées, et non d’une fonctionnalité de suivi automatique des positions fournie par Swift.
Créer un registre local complet de segments
Utilisez une cible en ligne de commande Swift pour macOS avec Foundation disponible. Remplacez son programme de niveau supérieur d’exemple par le programme complet ci-dessous. L’implémentation utilise append(contentsOf:), dont la signature Swift documentée accepte une autre valeur String. Consultez la référence append(contentsOf:) d’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.")
Ce programme a été examiné à la lumière de la documentation d’API récupérée ; il n’a pas été exécuté ici, car le shell disponible ne contient aucun exécutable Swift. Les préconditions incluses rendent explicite le comportement attendu de l’exemple lorsque vous l’exécutez sur votre Mac.
Ce que l’exemple est conçu pour vérifier
Les coordonnées attendues des segments sont calculées à partir des valeurs littérales d’entrée. L’accent combinant contribue une unité de code UTF-16, le fragment vide contribue zéro et le carré bleu contribue deux. Le suffixe final contribue cinq. Il s’agit de calculs illustratifs sur les entrées, et non de mesures de performance.
| Segment | Entrée | Position UTF-16 | Longueur UTF-16 |
|---|---|---|---|
| 0 | "cafe" |
0 | 4 |
| 1 | "\u{0301}" |
4 | 1 |
| 2 | "" |
5 | 0 |
| 3 | "\u{1F7E6}" |
5 | 2 |
| 4 | " done" |
7 | 5 |
Le fragment vide reçoit son propre numéro de segment, même s’il partage sa position avec le segment suivant. Cela préserve l’historique des ajouts sans inventer de texte. Le fait de renvoyer explicitement "" évite aussi un appel à une sous-chaîne Foundation dans ce cas.
La vérification de récupération compare volontairement des tableaux UTF-16. L’égalité des chaînes Swift reconnaît l’équivalence canonique, ce qui signifie que certaines chaînes ayant des séquences scalaires sous-jacentes différentes sont considérées comme égales. Une assertion exacte au niveau de l’encodage convient mieux au contrat de ce registre. Consultez la discussion sur la comparaison des chaînes dans Chaînes et caractères.
Pourquoi l’extraction utilise Foundation
Les entiers enregistrés sont convertis en NSRange et transmis directement à NSString.substring(with:). Apple indique que cette méthode extrait la plage spécifiée et exige que celle-ci reste dans les limites du récepteur. Les plages invalides peuvent provoquer une exception. Le registre contrôle à la fois le texte et ses plages, et rejette les numéros de segment inconnus avant l’extraction. Consultez substring(with:).
Chaque limite enregistrée provient de l’ajout d’une chaîne Swift complète. L’exemple ne crée jamais d’offset au milieu de la paire de substitution UTF-16 du carré bleu. Une limite peut toutefois se trouver à l’intérieur d’une séquence de caractères composés, comme le montre l’accent ; cette représentation sert précisément à récupérer les contributions exactes.
N’utilisez pas String.count pour construire ces plages Foundation. Apple définit NSString.length comme un nombre d’unités de code UTF-16, une mesure différente du nombre de caractères qui ne décrit pas la largeur du texte rendu. Consultez NSString.length.
Grouper les mutations, la création des segments et les instantanés
Pour un petit calcul synchrone, le type valeur suffit : un propriétaire ajoute les fragments et lit le résultat. Ses accesseurs privés empêchent les appelants de remplacer le texte assemblé indépendamment de la table des segments. Le total UTF-16 mis en cache évite également de recompter la chaîne assemblée, qui s’allonge à chaque ajout.
Si plusieurs tâches asynchrones doivent contribuer à un même registre, placez ce dernier derrière un acteur unique, ou gardez-le sur @MainActor lorsqu’il appartient à une interface Mac. Effectuez l’ajout et la mise à jour des métadonnées dans une même méthode synchrone isolée. Swift indique qu’un travail synchrone isolé par un acteur ne peut pas être interrompu à un point de suspension au sein de cette méthode. Consultez Concurrence.
Renvoyez le texte et les métadonnées des segments ensemble dans un seul instantané de valeur. Évitez d’attendre séparément la lecture du texte et celle de la table des segments : un autre ajout pourrait survenir entre ces appels. Les déclarations Sendable de l’exemple décrivent des valeurs pouvant franchir des frontières d’isolation ; elles ne sérialisent pas les écritures concurrentes dans une variable partagée. La documentation Sendable de Swift explique la distinction entre les valeurs transférables et l’état mutable protégé.
Si l’ordre des contributions est important, établissez cet ordre avant l’ajout. Un acteur protège la cohérence interne du registre ; l’application doit toujours décider quel fragment vient en premier.
Savoir quand ces décalages ne répondent plus à la tâche
Suivez cette séquence de diagnostic avant de réutiliser une plage :
- Vérifiez la propriété. Le numéro de segment appartient-il à ce registre ou à l’un de ses descendants compatibles en ajout uniquement ?
- Vérifiez l’historique des modifications. Quelque chose a-t-il changé avant la plage enregistrée ? Si oui, reconstruisez le registre ou adoptez un modèle tenant compte des modifications.
- Vérifiez les unités. Les coordonnées du consommateur sont-elles en unités de code UTF-16 ? Ne les remplacez pas par des nombres de caractères ou d’octets UTF-8.
- Vérifiez le résultat recherché. Récupérez-vous exactement la contribution ajoutée ou sélectionnez-vous un caractère affiché entier ? Ces opérations peuvent nécessiter des limites différentes.
Une sélection orientée caractères ne peut pas toujours représenter une seule contribution ajoutée. L’accent appartient au même caractère affiché que la lettre précédente. Si la tâche sur ordinateur consiste à surligner le texte, choisissez une règle de présentation explicite : étendez la sélection au caractère contenant la contribution, ou affichez celle-ci séparément dans une liste de segments. L’extension inclut du texte d’un segment antérieur ; conservez donc les décalages d’origine pour une récupération exacte.
Pour suivre exactement les ajouts, l’invariant utile est simple : gardez le préfixe inchangé, nommez les unités de coordonnées et associez chaque plage à son texte. Avec ce contrat, le registre peut récupérer les contributions antérieures après des ajouts ultérieurs sans conserver un seul String.Index à travers une mutation.
Sources
Collection — Apple.
Chaînes et caractères — Swift.org.
String — Apple.
NSString — Apple.
append(contentsOf:) — Apple.
substring(with:) — Apple.
length — Apple.
Concurrence — Swift.org.
Sendable — Swift.org.