Stockage de texte encodé en UTF-8 avec des chaînes de caractères

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Storing UTF-8 Encoded Text With Strings. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons aborder les complexités des chaînes de caractères en Rust, en particulier en relation avec l'encodage UTF-8, ainsi que les opérations et les différences du type String par rapport à d'autres collections.

Storing UTF-8 Encoded Text with Strings

Nous avons parlé des chaînes de caractères au chapitre 4, mais nous allons maintenant les examiner plus en détail. Les nouveaux Rustaceans se retrouvent souvent bloqués sur les chaînes de caractères pour une combinaison de trois raisons : la tendance de Rust à exposer les erreurs possibles, les chaînes de caractères étant une structure de données plus complexe que ce que beaucoup de programmeurs pensent, et l'UTF-8. Ces facteurs se combinent de manière qui peut sembler difficile lorsqu'on vient d'autres langages de programmation.

Nous abordons les chaînes de caractères dans le contexte des collections car les chaînes de caractères sont implémentées comme une collection d'octets, plus quelques méthodes pour fournir des fonctionnalités utiles lorsque ces octets sont interprétés comme du texte. Dans cette section, nous parlerons des opérations sur String que possède chaque type de collection, telles que la création, la mise à jour et la lecture. Nous aborderons également les façons dont String diffère des autres collections, à savoir comment l'indexation dans une String est compliquée par les différences entre l'interprétation des données String par les humains et les ordinateurs.

What Is a String?

Nous allons d'abord définir ce que nous entendons par le terme string. Rust n'a qu'un seul type de chaîne de caractères dans le langage de base, qui est la tranche de chaîne str qui est généralement vue sous sa forme empruntée &str. Au chapitre 4, nous avons parlé des tranches de chaîne de caractères, qui sont des références à des données de chaîne de caractères encodées en UTF-8 stockées ailleurs. Les littéraux de chaîne de caractères, par exemple, sont stockés dans le binaire du programme et sont donc des tranches de chaîne de caractères.

Le type String, qui est fourni par la bibliothèque standard de Rust plutôt que codé dans le langage de base, est un type de chaîne de caractères encodé en UTF-8, pouvant croître, mutable et propriétaire. Lorsque les Rustaceans font référence à "chaînes de caractères" en Rust, ils peuvent faire référence soit au type String, soit à la tranche de chaîne de caractères &str, et non seulement à l'un de ces types. Bien que cette section porte principalement sur String, les deux types sont largement utilisés dans la bibliothèque standard de Rust, et à la fois String et les tranches de chaîne de caractères sont encodées en UTF-8.

Creating a New String

De nombreuses opérations disponibles avec Vec<T> sont également disponibles avec String car String est en fait implémentée comme un wrapper autour d'un vecteur d'octets avec quelques garanties supplémentaires, restrictions et capacités. Un exemple d'une fonction qui fonctionne de la même manière avec Vec<T> et String est la fonction new pour créer une instance, comme montré dans la Liste 8-11.

let mut s = String::new();

Liste 8-11: Création d'une String vide et nouvelle

Cette ligne crée une nouvelle chaîne de caractères vide appelée s, dans laquelle nous pouvons ensuite charger des données. Souvent, nous aurons des données initiales avec lesquelles nous voulons commencer la chaîne de caractères. Pour cela, nous utilisons la méthode to_string, qui est disponible pour tout type qui implémente le trait Display, comme les littéraux de chaîne de caractères. La Liste 8-12 montre deux exemples.

let data = "initial contents";

let s = data.to_string();

// la méthode fonctionne également directement sur un littéral :
let s = "initial contents".to_string();

Liste 8-12: Utilisation de la méthode to_string pour créer une String à partir d'un littéral de chaîne de caractères

Ce code crée une chaîne de caractères contenant initial contents.

Nous pouvons également utiliser la fonction String::from pour créer une String à partir d'un littéral de chaîne de caractères. Le code de la Liste 8-13 est équivalent au code de la Liste 8-12 qui utilise to_string.

let s = String::from("initial contents");

Liste 8-13: Utilisation de la fonction String::from pour créer une String à partir d'un littéral de chaîne de caractères

Parce que les chaînes de caractères sont utilisées pour de nombreuses choses, nous pouvons utiliser de nombreuses API génériques différentes pour les chaînes de caractères, nous offrant ainsi un grand nombre d'options. Certaines d'entre elles peuvent sembler redondantes, mais elles ont toutes leur place! Dans ce cas, String::from et to_string font la même chose, donc laquelle choisir est une question de style et de lisibilité.

Rappelez-vous que les chaînes de caractères sont encodées en UTF-8, donc nous pouvons y inclure toute donnée correctement encodée, comme montré dans la Liste 8-14.

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");

Liste 8-14: Stockage de salutations dans différentes langues dans des chaînes de caractères

Toutes ces valeurs sont des String valides.

Updating a String

Une String peut augmenter de taille et son contenu peut changer, tout comme le contenu d'un Vec<T>, si vous y insérez plus de données. De plus, vous pouvez utiliser commodément l'opérateur + ou la macro format! pour concaténer des valeurs String.

Appending to a String with push_str and push

Nous pouvons faire croître une String en utilisant la méthode push_str pour ajouter une tranche de chaîne de caractères, comme montré dans la Liste 8-15.

let mut s = String::from("foo");
s.push_str("bar");

Liste 8-15: Ajout d'une tranche de chaîne de caractères à une String en utilisant la méthode push_str

Après ces deux lignes, s contiendra foobar. La méthode push_str prend une tranche de chaîne de caractères car nous ne voulons pas nécessairement prendre la propriété du paramètre. Par exemple, dans le code de la Liste 8-16, nous voulons être en mesure d'utiliser s2 après avoir ajouté son contenu à s1.

let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");

Liste 8-16: Utilisation d'une tranche de chaîne de caractères après avoir ajouté son contenu à une String

Si la méthode push_str prenait la propriété de s2, nous ne serions pas en mesure d'afficher sa valeur sur la dernière ligne. Cependant, ce code fonctionne comme prévu!

La méthode push prend un seul caractère en tant que paramètre et l'ajoute à la String. La Liste 8-17 ajoute la lettre l à une String en utilisant la méthode push.

let mut s = String::from("lo");
s.push('l');

Liste 8-17: Ajout d'un caractère à une valeur String en utilisant push

En conséquence, s contiendra lol.

Concatenation with the + Operator or the format! Macro

Souvent, vous voudrez combiner deux chaînes de caractères existantes. Une manière de le faire est d'utiliser l'opérateur +, comme montré dans la Liste 8-18.

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 a été déplacé ici et ne peut plus être utilisé

Liste 8-18: Utilisation de l'opérateur + pour combiner deux valeurs String en une nouvelle valeur String

La chaîne de caractères s3 contiendra Hello, world!. La raison pour laquelle s1 n'est plus valide après l'addition, et la raison pour laquelle nous avons utilisé une référence à s2, est liée à la signature de la méthode appelée lorsque nous utilisons l'opérateur +. L'opérateur + utilise la méthode add, dont la signature ressemble à ceci :

fn add(self, s: &str) -> String {

Dans la bibliothèque standard, vous verrez add définie à l'aide de types génériques et de types associés. Ici, nous avons remplacé les types concrets, ce qui se produit lorsque nous appelons cette méthode avec des valeurs String. Nous aborderons les types génériques au chapitre 10. Cette signature nous donne les indices dont nous avons besoin pour comprendre les points délicats de l'opérateur +.

Tout d'abord, s2 a un &, ce qui signifie que nous ajoutons une référence de la deuxième chaîne de caractères à la première chaîne de caractères. C'est en raison du paramètre s dans la fonction add : nous ne pouvons ajouter qu'un &str à une String ; nous ne pouvons pas additionner deux valeurs String ensemble. Mais attendez - le type de &s2 est &String, et non &str, comme spécifié dans le deuxième paramètre de add. Alors pourquoi la Liste 8-18 compile-t-elle?

La raison pour laquelle nous pouvons utiliser &s2 dans l'appel à add est que le compilateur peut coercer l'argument &String en un &str. Lorsque nous appelons la méthode add, Rust utilise une coercition de déréférencement, qui ici transforme &s2 en &s2[..]. Nous aborderons la coercition de déréférencement plus en détail au chapitre 15. Comme add ne prend pas la propriété du paramètre s, s2 sera toujours une String valide après cette opération.

Deuxièmement, nous pouvons voir dans la signature que add prend la propriété de self car self n'a pas d'&. Cela signifie que s1 dans la Liste 8-18 sera déplacé dans l'appel à add et ne sera plus valide après cela. Ainsi, bien que let s3 = s1 + &s2; semble copier les deux chaînes de caractères et créer une nouvelle, cette instruction prend effectivement la propriété de s1, ajoute une copie du contenu de s2, puis renvoie la propriété du résultat. En d'autres termes, il semble qu'elle fasse beaucoup de copies, mais ce n'est pas le cas ; l'implémentation est plus efficace que la copie.

Si nous devons concaténer plusieurs chaînes de caractères, le comportement de l'opérateur + devient difficile à gérer :

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

À ce stade, s sera tic-tac-toe. Avec tous les caractères + et ", il est difficile de voir ce qui se passe. Pour combiner les chaînes de caractères de manière plus complexe, nous pouvons au lieu de cela utiliser la macro format! :

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");

Ce code définit également s sur tic-tac-toe. La macro format! fonctionne comme println!, mais au lieu d'afficher la sortie à l'écran, elle renvoie une String avec le contenu. La version du code utilisant format! est beaucoup plus facile à lire, et le code généré par la macro format! utilise des références de sorte que cet appel ne prenne pas la propriété de l'un de ses paramètres.

Indexing into Strings

Dans de nombreux autres langages de programmation, accéder à des caractères individuels dans une chaîne de caractères en les référencant par indice est une opération valide et courante. Cependant, si vous essayez d'accéder à des parties d'une String en utilisant la syntaxe d'indexation en Rust, vous obtiendrez une erreur. Considérez le code invalide de la Liste 8-19.

let s1 = String::from("hello");
let h = s1[0];

Liste 8-19: Tentative d'utilisation de la syntaxe d'indexation avec une String

Ce code entraînera l'erreur suivante :

error[E0277]: the type `String` cannot be indexed by `{integer}`
 --> src/main.rs:3:13
  |
3 |     let h = s1[0];
  |             ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for
`String`

L'erreur et la note racontent l'histoire : les chaînes de caractères Rust ne prennent pas en charge l'indexation. Mais pourquoi pas? Pour répondre à cette question, nous devons discuter de la manière dont Rust stocke les chaînes de caractères en mémoire.

Internal Representation

Une String est un wrapper sur un Vec<u8>. Regardons quelques-unes de nos chaînes d'exemples correctement encodées en UTF-8 de la Liste 8-14. Tout d'abord, celle-ci :

let hello = String::from("Hola");

Dans ce cas, len sera 4, ce qui signifie que le vecteur stockant la chaîne "Hola" est de 4 octets de long. Chacune de ces lettres prend un octet lorsqu'elle est encodée en UTF-8. La ligne suivante, cependant, peut vous surprendre (notez que cette chaîne commence par la lettre cyrillique majuscule Ze, pas le chiffre arabe 3) :

let hello = String::from("Здравствуйте");

Si vous étiez demandé(e) quelle est la longueur de la chaîne, vous pourriez répondre 12. En fait, la réponse de Rust est 24 : c'est le nombre d'octets nécessaires pour encoder "Здравствуйте" en UTF-8, car chaque valeur scalaire Unicode dans cette chaîne prend 2 octets de stockage. Par conséquent, un indice dans les octets de la chaîne ne correspondra pas toujours à une valeur scalaire Unicode valide. Pour illustrer, considérez ce code Rust invalide :

let hello = "Здравствуйте";
let answer = &hello[0];

Vous savez déjà que answer ne sera pas З, la première lettre. Lorsqu'elle est encodée en UTF-8, le premier octet de З est 208 et le second est 151, de sorte que l'on pourrait penser que answer devrait en fait être 208, mais 208 n'est pas un caractère valide en soi. Retourner 208 n'est probablement pas ce que le(s) utilisateur(s) voudrait si elle/ils demandaient la première lettre de cette chaîne ; cependant, c'est la seule donnée que Rust a à l'indice d'octet 0. En général, les utilisateurs ne veulent pas que la valeur d'octet soit retournée, même si la chaîne ne contient que des lettres latines : si &"hello"[0] était un code valide qui retournait la valeur d'octet, il retournerait 104, pas h.

La réponse, donc, est que pour éviter de retourner une valeur inattendue et de causer des bugs qui pourraient ne pas être découverts immédiatement, Rust ne compile pas ce code du tout et empêche les malentendus dès le début du processus de développement.

Bytes and Scalar Values and Grapheme Clusters! Oh My!

Un autre point concernant UTF-8 est qu'il existe en fait trois façons pertinentes de considérer les chaînes de caractères du point de vue de Rust : en tant que bytes, en tant que valeurs scalaires et en tant que grappes de caractères (ce qui est le plus proche de ce que nous appellerions des lettres).

Si nous considérons le mot hindi "नमस्ते" écrit en script dévanagari, il est stocké sous forme d'un vecteur de valeurs u8 qui ressemble à ceci :

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224,
164, 164, 224, 165, 135]

Ce sont 18 octets et c'est ainsi que les ordinateurs stockent finalement ces données. Si nous les considérons comme des valeurs scalaires Unicode, qui sont le type char de Rust, ces octets ressemblent à ceci :

['न', 'म', 'स', '्', 'त', 'े']

Il y a six valeurs char ici, mais la quatrième et la sixième ne sont pas des lettres : ce sont des diacritiques qui n'ont pas de sens isolés. Enfin, si nous les considérons comme des grappes de caractères, nous obtiendrons ce qu'une personne appellerait les quatre lettres qui composent le mot hindi :

["न", "म", "स्", "ते"]

Rust fournit différentes façons d'interpréter les données brutes de chaîne de caractères stockées par les ordinateurs de sorte que chaque programme puisse choisir l'interprétation dont il a besoin, quelle que soit la langue humaine des données.

Une dernière raison pour laquelle Rust ne nous permet pas d'indexer une String pour obtenir un caractère est que les opérations d'indexation sont censées prendre toujours un temps constant (O(1)). Mais il n'est pas possible de garantir cette performance avec une String, car Rust devrait parcourir le contenu depuis le début jusqu'à l'index pour déterminer combien de caractères valides il y avait.

Slicing Strings

Indexer une chaîne de caractères est souvent une mauvaise idée car il n'est pas clair quel devrait être le type de retour de l'opération d'indexation de chaîne : une valeur d'octet, un caractère, une grappe de caractères ou une tranche de chaîne. Si vous avez vraiment besoin d'utiliser des indices pour créer des tranches de chaîne, donc, Rust vous demande d'être plus précis.

Plutôt qu'indexer en utilisant [] avec un seul nombre, vous pouvez utiliser [] avec une plage pour créer une tranche de chaîne contenant des octets particuliers :

let hello = "Здравствуйте";

let s = &hello[0..4];

Ici, s sera un &str qui contient les quatre premiers octets de la chaîne. Plus tôt, nous avons mentionné que chacun de ces caractères était de deux octets, ce qui signifie que s sera Зд.

Si nous essayions de découper seulement une partie des octets d'un caractère avec quelque chose comme &hello[0..1], Rust planterait à l'exécution de la même manière qu'il le ferait si un indice invalide était consulté dans un vecteur :

thread 'main' panicked at 'byte index 1 is not a char boundary;
it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/main.rs:4:14

Vous devriez faire preuve de prudence lorsqu'il s'agit de créer des tranches de chaîne avec des plages, car cela peut faire planter votre programme.

Methods for Iterating Over Strings

La meilleure façon de travailler sur des parties de chaînes de caractères est d'être explicite sur le fait que vous voulez des caractères ou des octets. Pour les valeurs scalaires Unicode individuelles, utilisez la méthode chars. Appeler chars sur "Зд" sépare et renvoie deux valeurs de type char, et vous pouvez itérer sur le résultat pour accéder à chaque élément :

for c in "Зд".chars() {
    println!("{c}");
}

Ce code affichera ce qui suit :

З
д

Alternativement, la méthode bytes renvoie chaque octet brut, ce qui peut être approprié pour votre domaine :

for b in "Зд".bytes() {
    println!("{b}");
}

Ce code affichera les quatre octets qui composent cette chaîne :

208
151
208
180

Mais n'oubliez pas que les valeurs scalaires Unicode valides peuvent être composées de plus d'un octet.

Obtenir des grappes de caractères à partir de chaînes de caractères, comme avec le script dévanagari, est complexe, donc cette fonctionnalité n'est pas fournie par la bibliothèque standard. Des crates sont disponibles sur https://crates.io si c'est la fonctionnalité dont vous avez besoin.

Strings Are Not So Simple

Pour résumer, les chaînes de caractères sont complexes. Différentes langues de programmation font des choix différents quant à la manière de présenter cette complexité au programmeur. Rust a choisi de faire du bon traitement des données String le comportement par défaut pour tous les programmes Rust, ce qui signifie que les programmeurs doivent réfléchir davantage au traitement des données UTF-8 dès le départ. Ce compromis expose davantage la complexité des chaînes de caractères que ne le fait apparaître d'autres langues de programmation, mais cela vous empêche d'avoir à gérer des erreurs liées à des caractères non ASCII plus tard dans votre cycle de développement.

La bonne nouvelle est que la bibliothèque standard offre beaucoup de fonctionnalités basées sur les types String et &str pour aider à gérer correctement ces situations complexes. N'oubliez pas de consulter la documentation pour des méthodes utiles telles que contains pour effectuer une recherche dans une chaîne et replace pour substituer des parties d'une chaîne par une autre chaîne.

Passons à quelque chose un peu moins complexe : les tableaux de hachage!

Summary

Félicitations! Vous avez terminé le laboratoire Storing UTF-8 Encoded Text With Strings. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.