Contactez-nous

Les chaînes de caractères : `String` et `&str` (introduction)

Découvrez les deux types de chaînes en Rust : `String` (possédé, mutable) et `&str` (tranche, immuable). Apprenez leurs différences et usages.

Le duo dynamique des chaînes en Rust : `String` et `&str`

La manipulation de texte est une tâche omniprésente en programmation. Rust, avec son accent sur la sécurité et la performance, aborde la gestion des chaînes de caractères d'une manière qui peut sembler unique aux nouveaux venus, mais qui est profondément ancrée dans son système d'ownership. Au lieu d'un seul type de chaîne universel, Rust en propose principalement deux : `String` et `&str` (prononcé "string slice"). Comprendre la distinction entre ces deux types est fondamental pour écrire du code Rust correct et efficace.

Ce sous-chapitre introduit ces deux types de chaînes. Nous explorerons ce qui les différencie en termes de propriété des données, de mutabilité, d'allocation mémoire, et comment ils interagissent. Vous verrez que `String` est un type de chaîne possédé et modifiable, tandis que `&str` est une vue (une "tranche") immuable sur des données de chaîne, qui peuvent exister ailleurs.

Cette dualité, loin d'être une complication, offre une flexibilité et un contrôle précis sur la manière dont le texte est géré, permettant d'éviter des allocations inutiles et de garantir la sécurité mémoire sans garbage collector. C'est une pierre angulaire du travail avec le texte en Rust.

`String` : la chaîne possédée, modifiable et allouée sur le tas

Le type `String` en Rust représente une chaîne de caractères possédée, allouée sur le tas. Cela signifie que lorsque vous créez une `String`, la mémoire nécessaire pour stocker son contenu textuel est demandée au système d'exploitation au moment de l'exécution. Une `String` est garantie d'être encodée en UTF-8, ce qui lui permet de stocker une vaste gamme de caractères issus de différentes langues et systèmes d'écriture.

Etant donné que `String` possède ses données, elle est responsable de leur gestion, y compris de leur libération lorsque la `String` sort de sa portée (grâce au système d'ownership de Rust). Une `String` est également modifiable (mutable), ce qui signifie que vous pouvez y ajouter des caractères, en supprimer, ou la modifier d'autres manières après sa création.

Voici quelques façons courantes de créer et de manipuler des `String` :

fn main() {
    // Créer une String vide
    let mut s1 = String::new();
    println!("s1 (vide) : '{}'", s1);

    // Créer une String à partir d'un littéral de chaîne (&str)
    let data = "contenu initial";
    let s2 = data.to_string(); // Méthode disponible sur les &str
    println!("s2 (depuis .to_string()) : '{}'", s2);

    let s3 = String::from("autre contenu"); // Méthode `from` directement sur String
    println!("s3 (depuis String::from) : '{}'", s3);

    // Modifier une String (elle doit être déclarée mutable)
    let mut s_modifiable = String::from("Bonjour");
    s_modifiable.push_str(" le monde"); // push_str ajoute une tranche de chaîne (&str)
    println!("s_modifiable (après push_str) : '{}'", s_modifiable);

    s_modifiable.push('!'); // push ajoute un seul caractère (char)
    println!("s_modifiable (après push) : '{}'", s_modifiable);
}

Vous utiliserez typiquement `String` lorsque vous avez besoin de données textuelles dont vous devez être le propriétaire, ou lorsque vous avez besoin de modifier ou de construire une chaîne de caractères dynamiquement au cours de l'exécution de votre programme. Par exemple, lire une entrée utilisateur, construire un message à partir de plusieurs morceaux, ou stocker du texte qui doit perdurer au-delà d'une simple référence.

`&str` : la tranche de chaîne, une vue immuable sur des données textuelles

Le type `&str`, souvent appelé "string slice" ou simplement "str slice", représente une référence (ou "emprunt") à une séquence d'octets qui sont garantis d'être une chaîne de caractères valide au format UTF-8. Contrairement à `String`, un `&str` ne possède pas les données textuelles elles-mêmes ; il pointe vers des données qui sont stockées ailleurs.

Les littéraux de chaîne que vous écrivez directement dans votre code (par exemple, `"Hello, Rust!"`) sont de type `&'static str`. Le `'static` indique que les données de la chaîne sont stockées directement dans le binaire de votre programme et vivent pendant toute sa durée d'exécution. Un `&str` est une vue immuable sur ces données, vous ne pouvez donc pas modifier le contenu d'un littéral de chaîne via un `&str`.

Un `&str` peut également pointer vers une partie d'une `String` (ou d'un autre `&str`). C'est ce qu'on appelle un "slice" (une tranche). Voici comment cela fonctionne :

fn main() {
    let phrase = String::from("Apprendre Rust est amusant.");

    // Créer des tranches de la String `phrase`
    let mot1: &str = &phrase[0..8]; // Tranche du premier mot "Apprendre"
                                  // Les indices sont basés sur les octets, attention avec l'UTF-8 !
    let mot2 = &phrase[9..13];    // Tranche du mot "Rust"

    println!("Phrase complète : '{}'", phrase);
    println!("Mot 1 : '{}'", mot1);
    println!("Mot 2 : '{}'", mot2);

    let literal_example = "Ceci est un littéral de chaîne."; // Type: &'static str
    println!("Littéral : '{}'", literal_example);

    // On peut aussi prendre une tranche d'un littéral (bien que moins courant car il est déjà un &str)
    let partie_literal = &literal_example[0..4]; // "Ceci"
    println!("Partie du littéral : '{}'", partie_literal);
}

Attention avec le découpage (slicing) : Les indices pour créer une tranche `&str` à partir d'une `String` (ou d'un autre `&str`) sont basés sur les octets. Si une tranche ne commence ou ne se termine pas sur une frontière de caractère UTF-8 valide, le programme entrera en `panic`. Par exemple, un caractère emoji peut prendre plusieurs octets. Il est donc plus sûr d'utiliser des méthodes d'itération sur les caractères ou des méthodes spécifiques pour le découpage si vous n'êtes pas sûr des frontières d'octets.

Les `&str` sont extrêmement utiles car ils permettent de travailler avec des portions de texte sans avoir besoin de copier les données. Quand vous passez du texte à une fonction qui a seulement besoin de le lire (et non de le modifier ou d'en prendre possession), il est idiomatique d'utiliser `&str` comme type de paramètre. Cela rend la fonction plus flexible car elle peut accepter :

  • Des littéraux de chaîne (qui sont déjà des `&str`).
  • Des références à des `String` (une `&String` peut être automatiquement "coercée" ou convertie en `&str`).
  • D'autres `&str`.

Exemple avec une fonction prenant `&str` :

fn afficher_longueur(s: &str) {
    println!("La chaîne '{}' a une longueur de {} octets.", s, s.len());
} 

fn main() {
    let ma_string = String::from("Exemple");
    let mon_literal = "Test";

    afficher_longueur(&ma_string); // Passe une référence à une String
    afficher_longueur(mon_literal); // Passe un littéral de chaîne

    // La méthode .len() sur un &str retourne le nombre d'octets, pas nécessairement le nombre de caractères
    // pour les chaînes UTF-8 complexes.
    let emoji_str = "💖";
    println!("L'emoji '{}' a {} octets et {} caractères.", 
             emoji_str, emoji_str.len(), emoji_str.chars().count()); // .chars().count() pour le nombre de caractères Unicode
}

Quand utiliser `String` vs `&str` ? Points clés et coercition

Le choix entre `String` et `&str` dépend de ce que vous voulez faire avec les données textuelles :

  • Utilisez `String` si :
    • Vous avez besoin de créer ou de modifier des données textuelles au moment de l'exécution.
    • Vous avez besoin que les données textuelles soient possédées par la structure ou la variable actuelle (par exemple, un champ dans une `struct` qui doit détenir sa propre chaîne).
    • Une fonction a besoin de retourner une chaîne qu'elle a générée.
  • Utilisez `&str` si :
    • Vous travaillez avec des littéraux de chaîne.
    • Vous avez seulement besoin d'une vue (lecture seule) sur des données textuelles existantes (qui peuvent être dans une `String` ou un autre `&str`).
    • Vous écrivez une fonction qui accepte des données textuelles en lecture seule ; prendre `&str` rendra la fonction plus polyvalente.

Rust effectue une opération appelée coercition de déréférencement (deref coercion) qui permet de passer une référence à une `String` (`&String`) à une fonction qui attend un `&str`. C'est parce que `String` implémente le trait `Deref` de telle sorte qu'elle peut être déréférencée en un `&str`. Cette commodité rend le travail avec les deux types plus fluide.

fn traiter_texte(texte: &str) {
    println!("Traitement de : {}", texte);
}

fn main() {
    let s_proprietaire = String::from("Je suis une String.");
    let s_literal = "Je suis un &str.";

    traiter_texte(&s_proprietaire); // &String est automatiquement coercé en &str
    traiter_texte(s_literal);       // &str est passé directement
}

En résumé, `String` est le conteneur de données de chaîne, tandis que `&str` est une vue sur ces données (ou sur des données statiques). Cette distinction, initialement déroutante, est une conséquence directe du système d'ownership et d'emprunt de Rust, et elle est essentielle pour écrire du code sûr et performant qui gère efficacement la mémoire. La plupart du temps, les fonctions devraient préférer accepter `&str` pour une flexibilité maximale, et retourner `String` si elles créent de nouvelles données textuelles.