Le type de tranche

Beginner

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

Introduction

Bienvenue dans Le type Slice. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons résoudre un problème de programmation en écrivant une fonction qui prend une chaîne de mots séparés par des espaces et renvoie le premier mot qu'elle trouve dans cette chaîne. Ensuite, nous discuterons des limites de l'utilisation d'indices pour représenter des sous-chaînes et de la solution à ce problème en utilisant des tranches de chaîne en Rust.

Le type Slice

Les tranches vous permettent de référencer une séquence contiguë d'éléments dans une collection plutôt que la collection entière. Une tranche est un type de référence, donc elle n'a pas de propriété.

Voici un petit problème de programmation : écrire une fonction qui prend une chaîne de mots séparés par des espaces et renvoie le premier mot qu'elle trouve dans cette chaîne. Si la fonction ne trouve pas d'espace dans la chaîne, toute la chaîne doit être un mot, donc la chaîne entière devrait être renvoyée.

Examillons comment écrire la signature de cette fonction sans utiliser de tranches, pour comprendre le problème que les tranches résoudront :

fn first_word(s: &String) ->?

La fonction first_word a un paramètre &String. Nous ne voulons pas la propriété, donc c'est correct. Mais que devrions-nous renvoyer? Nous n'avons pas vraiment de moyen de parler d'une partie d'une chaîne. Cependant, nous pourrions renvoyer l'index de la fin du mot, indiqué par un espace. Essayons cela, comme montré dans la Liste 4-7.

Nom de fichier : src/main.rs

fn first_word(s: &String) -> usize {
  1 let bytes = s.as_bytes();

    for (2 i, &item) in 3 bytes.iter().enumerate() {
      4 if item == b' ' {
            return i;
        }
    }

  5 s.len()
}

Liste 4-7 : La fonction first_word qui renvoie une valeur d'index de byte dans le paramètre String

Comme nous devons parcourir l'élément String un par un et vérifier si une valeur est un espace, nous allons convertir notre String en un tableau d'octets en utilisant la méthode as_bytes [1].

Ensuite, nous créons un itérateur sur le tableau d'octets en utilisant la méthode iter [3]. Nous aborderons les itérateurs en détail au Chapitre 13. Pour l'instant, sachez que iter est une méthode qui renvoie chaque élément d'une collection et que enumerate enveloppe le résultat de iter et renvoie chaque élément sous forme d'un tuple au lieu de cela. Le premier élément du tuple renvoyé par enumerate est l'index, et le second élément est une référence à l'élément. Cela est un peu plus pratique que de calculer l'index nous-mêmes.

Comme la méthode enumerate renvoie un tuple, nous pouvons utiliser des motifs pour déstructurer ce tuple. Nous aborderons les motifs plus en détail au Chapitre 6. Dans la boucle for, nous spécifions un motif qui a i pour l'index dans le tuple et &item pour le seul octet dans le tuple [2]. Comme nous obtenons une référence à l'élément de .iter().enumerate(), nous utilisons & dans le motif.

À l'intérieur de la boucle for, nous cherchons l'octet qui représente l'espace en utilisant la syntaxe littérale d'octet [4]. Si nous trouvons un espace, nous renvoyons la position. Sinon, nous renvoyons la longueur de la chaîne en utilisant s.len() [5].

Nous avons maintenant un moyen de trouver l'index de la fin du premier mot dans la chaîne, mais il y a un problème. Nous renvoyons un usize tout seul, mais ce n'est qu'un nombre significatif dans le contexte du &String. En d'autres termes, parce que c'est une valeur séparée de la String, il n'est pas garanti qu'elle sera toujours valide à l'avenir. Considérez le programme dans la Liste 4-8 qui utilise la fonction first_word de la Liste 4-7.

// src/main.rs
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word aura la valeur 5

    s.clear(); // cela vide la String, la rendant égale à ""

    // word a toujours la valeur 5 ici, mais il n'y a plus de chaîne que
    // nous pourrions utiliser de manière significative avec la valeur 5. word est maintenant totalement invalide!
}

Liste 4-8 : Stockage du résultat de l'appel de la fonction first_word puis modification du contenu de la String

Ce programme se compile sans erreur et le ferait également si nous utilisions word après avoir appelé s.clear(). Parce que word n'est pas du tout lié à l'état de s, word contient toujours la valeur 5. Nous pourrions utiliser cette valeur 5 avec la variable s pour essayer d'extraire le premier mot, mais ce serait un bogue car le contenu de s a changé depuis que nous avons enregistré 5 dans word.

Devoir vous soucier de l'index dans word qui se met hors de synchronisation avec les données dans s est fastidieux et propice aux erreurs! Gérer ces indices est encore plus fragile si nous écrivons une fonction second_word. Sa signature devrait ressembler à cela :

fn second_word(s: &String) -> (usize, usize) {

Maintenant, nous suivons un index de départ et un index de fin, et nous avons encore plus de valeurs qui ont été calculées à partir des données dans un état particulier mais qui ne sont pas liées à cet état du tout. Nous avons trois variables non liées qui circulent et qui doivent être maintenues en synchronisation.

Heureusement, Rust a une solution à ce problème : les tranches de chaîne.

Tranches de chaîne

Une tranche de chaîne est une référence à une partie d'une String, et elle ressemble à ceci :

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

Plutôt qu'une référence à l'ensemble de la String, hello est une référence à une partie de la String, spécifiée dans le morceau supplémentaire [0..5]. Nous créons des tranches en utilisant une plage entre crochets en spécifiant [index_de_début..index_de_fin], où index_de_début est la première position dans la tranche et index_de_fin est une unité plus grande que la dernière position dans la tranche. Internement, la structure de données de la tranche stocke la position de départ et la longueur de la tranche, qui correspond à index_de_fin moins index_de_début. Ainsi, dans le cas de let world = &s[6..11];, world serait une tranche qui contient un pointeur vers le byte à l'index 6 de s avec une valeur de longueur de 5.

La Figure 4-6 montre cela dans un diagramme.

Figure 4-6 : Tranche de chaîne faisant référence à une partie d'une String

Avec la syntaxe de plage .. de Rust, si vous voulez commencer à l'index 0, vous pouvez omettre la valeur avant les deux points. En d'autres termes, ces deux expressions sont équivalentes :

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

let slice = &s[0..2];
let slice = &s[..2];

De même, si votre tranche inclut le dernier byte de la String, vous pouvez omettre le nombre final. Cela signifie que ces deux expressions sont équivalentes :

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

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

Vous pouvez également omettre les deux valeurs pour prendre une tranche de toute la chaîne. Donc, ces deux expressions sont équivalentes :

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

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

Note : Les indices de plage de tranches de chaîne doivent se trouver aux limites valides des caractères UTF-8. Si vous essayez de créer une tranche de chaîne au milieu d'un caractère multioctet, votre programme se terminera avec une erreur. Dans le but d'introduire les tranches de chaîne, nous supposons ici uniquement des caractères ASCII ; une discussion plus approfondie du traitement UTF-8 se trouve dans "Stockage de texte encodé en UTF-8 avec des chaînes".

Ayant toutes ces informations à l'esprit, réécrivons first_word pour renvoyer une tranche. Le type qui signifie "tranche de chaîne" est écrit &str :

Nom de fichier : src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Nous obtenons l'index de la fin du mot de la même manière que dans la Liste 4-7, en cherchant la première occurrence d'un espace. Lorsque nous trouvons un espace, nous renvoyons une tranche de chaîne en utilisant le début de la chaîne et l'index de l'espace comme indices de début et de fin.

Maintenant, lorsque nous appelons first_word, nous obtenons une seule valeur qui est liée aux données sous-jacentes. La valeur est composée d'une référence au point de départ de la tranche et du nombre d'éléments dans la tranche.

Renvoyer une tranche fonctionnerait également pour une fonction second_word :

fn second_word(s: &String) -> &str {

Nous avons maintenant une API simple qui est beaucoup plus difficile à foirer car le compilateur assurera que les références dans la String restent valides. Rappelez-vous le bogue dans le programme de la Liste 4-8, lorsque nous avons obtenu l'index de la fin du premier mot mais avons ensuite vidé la chaîne, rendant ainsi notre index invalide? Ce code était logiquement incorrect mais n'a pas montré d'erreurs immédiates. Les problèmes se seraient manifestés plus tard si nous avions continué à utiliser l'index du premier mot avec une chaîne vidée. Les tranches rendent ce bogue impossible et nous permettent de détecter un problème dans notre code bien plus tôt. Utiliser la version avec tranche de first_word entraînera une erreur de compilation :

Nom de fichier : src/main.rs

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // erreur!

    println!("le premier mot est : {word}");
}

Voici l'erreur du compilateur :

error[E0502]: impossible de prêter mutuellement `s` car il est déjà prêté en lecture seule
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- prêt en lecture seule ici
17 |
18 |     s.clear(); // erreur!
   |     ^^^^^^^^^ prêt mutuel ici
19 |
20 |     println!("le premier mot est : {word}");
   |                                   ---- prêt en lecture seule utilisé plus tard ici

Rappelez-vous les règles d'emprunt : si nous avons une référence immuable à quelque chose, nous ne pouvons pas également prendre une référence mutable. Parce que clear doit tronquer la String, il a besoin d'obtenir une référence mutable. L'instruction println! après l'appel à clear utilise la référence dans word, donc la référence immuable doit encore être active à ce moment-là. Rust interdit la référence mutable dans clear et la référence immuable dans word d'exister en même temps, et la compilation échoue. Non seulement Rust a rendu notre API plus facile à utiliser, mais il a également éliminé une classe entière d'erreurs au moment de la compilation!

Les littéraux de chaîne comme tranches

Rappelez-vous que nous avons parlé des littéraux de chaîne étant stockés dans le binaire. Maintenant que nous connaissons les tranches, nous pouvons correctement comprendre les littéraux de chaîne :

let s = "Hello, world!";

Le type de s ici est &str : c'est une tranche pointant vers ce point spécifique du binaire. C'est également pourquoi les littéraux de chaîne sont immuables ; &str est une référence immutable.

Les tranches de chaîne comme paramètres

Savoir que l'on peut prendre des tranches de littéraux et de valeurs String nous conduit à une autre amélioration de first_word, et c'est sa signature :

fn first_word(s: &String) -> &str {

Un Rustacean plus expérimenté écrirait plutôt la signature montrée dans la Liste 4-9 car cela nous permet d'utiliser la même fonction sur des valeurs &String et des valeurs &str.

fn first_word(s: &str) -> &str {

Liste 4-9 : Amélioration de la fonction first_word en utilisant une tranche de chaîne pour le type du paramètre s

Si nous avons une tranche de chaîne, nous pouvons la passer directement. Si nous avons une String, nous pouvons passer une tranche de la String ou une référence à la String. Cette flexibilité profite des coercitions de déréférencement, une fonctionnalité que nous aborderons dans "Les coercitions de déréférencement implicites avec les fonctions et les méthodes".

Définir une fonction pour prendre une tranche de chaîne plutôt qu'une référence à une String rend notre API plus générale et utile sans perdre aucune fonctionnalité :

Nom de fichier : src/main.rs

fn main() {
    let my_string = String::from("hello world");

    // `first_word` fonctionne sur des tranches de `String`, que ce soit
    // partielle ou complète
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` fonctionne également sur des références à des `String`, qui
    // sont équivalentes à des tranches complètes de `String`
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` fonctionne sur des tranches de littéraux de chaîne,
    // que ce soit partielle ou complète
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Parce que les littéraux de chaîne *sont* déjà des tranches de chaîne,
    // cela fonctionne également, sans la syntaxe de tranche!
    let word = first_word(my_string_literal);
}

Autres tranches

Les tranches de chaîne, comme vous pouvez l'imaginer, sont spécifiques aux chaînes. Mais il existe également un type de tranche plus général. Considérez ce tableau :

let a = [1, 2, 3, 4, 5];

De même que nous pouvons vouloir faire référence à une partie d'une chaîne, nous pouvons vouloir faire référence à une partie d'un tableau. Nous le ferions comme ceci :

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Cette tranche a le type &[i32]. Elle fonctionne de la même manière que les tranches de chaîne, en stockant une référence au premier élément et une longueur. Vous utiliserez ce type de tranche pour toutes sortes d'autres collections. Nous aborderons ces collections en détail lorsque nous parlerons des vecteurs au Chapitre 8.

Sommaire

Félicitations! Vous avez terminé le laboratoire Le type de tranche. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.