Le type slice

Une slice vous permet d'obtenir une référence vers une séquence continue d'éléments d'une collection plutôt que toute la collection. Une slice est un genre de référence, donc elle ne prend pas possession.

Voici un petit problème de programmation : écrire une fonction qui prend une chaîne de caractères et retourne le premier mot qu'elle trouve dans cette chaîne. Si la fonction ne trouve pas d'espace dans la chaîne, cela veut dire que la chaîne est en un seul mot, donc la chaîne en entier doit être retournée.

Voyons comment écrire la signature de cette fonction sans utiliser les slices, afin de comprendre le problème que règlent les slices :

fn premier_mot(s: &String) -> ?

La fonction premier_mot prend un &String comme paramètre. Nous ne voulons pas en prendre possession, donc c'est ce qu'il nous faut. Mais que devons-nous retourner ? Nous n'avons aucun moyen de désigner une partie d'une chaîne de caractères. Cependant, nous pouvons retourner l'indice de la fin du mot, qui se produit lorsqu'il y a un espace. Essayons cela, dans l'encart 4-7 :

Fichier : src/main.rs

fn premier_mot(s: &String) -> usize {
    let octets = s.as_bytes();

    for (i, &element) in octets.iter().enumerate() {
        if element == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Encart 4-7 : La fonction premier_mot qui retourne l'indice d'un octet provenant du paramètre String

Comme nous avons besoin de parcourir la String élément par élément et de vérifier si la valeur est une espace, nous convertissons notre String en un tableau d'octets en utilisant la méthode as_bytes :

fn premier_mot(s: &String) -> usize {
    let octets = s.as_bytes();

    for (i, &element) in octets.iter().enumerate() {
        if element == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Ensuite, nous créons un itérateur sur le tableau d'octets en utilisant la méthode iter :

fn premier_mot(s: &String) -> usize {
    let octets = s.as_bytes();

    for (i, &element) in octets.iter().enumerate() {
        if element == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Nous aborderons plus en détail les itérateurs dans le chapitre 13. Pour le moment, sachez que iter est une méthode qui retourne chaque élément d'une collection, et que enumerate transforme le résultat de iter pour retourner plutôt chaque élément comme un tuple. Le premier élément du tuple retourné par enumerate est l'indice, et le second élément est une référence vers l'élément. C'est un peu plus pratique que de calculer les indices par nous-mêmes.

Comme la méthode enumerate retourne un tuple, nous pouvons utiliser des motifs pour déstructurer ce tuple. Nous verrons les motifs au chapitre 6. Dans la boucle for, nous précisons un motif qui indique que nous définissons i pour l'indice au sein du tuple et &element pour l'octet dans le tuple. Comme nous obtenons une référence vers l'élément avec .iter().enumerate(), nous utilisons & dans le motif.

Au sein de la boucle for, nous recherchons l'octet qui représente l'espace en utilisant la syntaxe de littéral d'octet. Si nous trouvons une espace, nous retournons sa position. Sinon, nous retournons la taille de la chaîne en utilisant s.len() :

fn premier_mot(s: &String) -> usize {
    let octets = s.as_bytes();

    for (i, &element) in octets.iter().enumerate() {
        if element == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Nous avons maintenant une façon de trouver l'indice de la fin du premier mot dans la chaîne de caractères, mais il y a un problème. Nous retournons un usize tout seul, mais il n'a du sens que lorsqu'il est lié au &String. Autrement dit, comme il a une valeur séparée de la String, il n'y a pas de garantie qu'il restera toujours valide dans le futur. Imaginons le programme dans l'encart 4-8 qui utilise la fonction premier_mot de l'encart 4-7 :

Fichier : src/main.rs

fn premier_mot(s: &String) -> usize {
    let octets = s.as_bytes();

    for (i, &element) in octets.iter().enumerate() {
        if element == b' ' {
            return i;
        }
    }

    s.len()
}

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

    let mot = premier_mot(&s); // la variable mot aura 5 comme valeur.

    s.clear(); // ceci vide la String, elle vaut maintenant "".

    // mot a toujours la valeur 5 ici, mais il n'y a plus de chaîne qui donne
    // du sens à la valeur 5. mot est maintenant complètement invalide !
}

Encart 4-8 : On stocke le résultat de l'appel à la fonction premier_mot et ensuite on change le contenu de la String

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

Se préoccuper en permanence que l'indice présent dans mot ne soit plus synchronisé avec les données présentes dans s est fastidieux et source d'erreur ! La gestion de ces indices est encore plus risquée si nous écrivons une fonction second_mot. Sa signature ressemblerait à ceci :

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

Maintenant, nous avons un indice de début et un indice de fin, donc nous avons encore plus de valeurs qui sont calculées à partir d'une donnée dans un état donné, mais qui ne sont pas liées du tout à l'état de cette donnée. Nous avons trois variables isolées qui ont besoin d'être maintenues à jour.

Heureusement, Rust a une solution pour ce problème : les slices de chaînes de caractères.

Les slices de chaînes de caractères

Une slice de chaîne de caractères (ou slice de chaîne) est une référence à une partie d'une String, et ressemble à ceci :

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

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

Plutôt que d'être une référence vers toute la String, hello est une référence vers une partie de la String, comme indiqué dans la partie supplémentaire [0..5]. Nous créons des slices en utilisant un intervalle entre crochets en spécifiant [indice_debut..indice_fin], où indice_debut est la position du premier octet de la slice et indice_fin est la position juste après le dernier octet de la slice. En interne, la structure de données de la slice stocke la position de départ et la longueur de la slice, ce qui correspond à indice_fin moins indice_debut. Donc dans le cas de let world = &s[6..11];, world est une slice qui contient un pointeur vers le sixième octet de s et une longueur de 5.

L'illustration 4-6 montre ceci dans un schéma.

world contient un pointeur vers l'octet d'indice 6 de la String s et
une longueur de 5

Illustration 4-6 : Une slice de chaîne qui pointe vers une partie d'une String

Avec la syntaxe d'intervalle .. de Rust, si vous voulez commencer à l'indice zéro, vous pouvez ne rien mettre avant les deux points. Autrement dit, ces deux cas sont identiques :


#![allow(unused)]
fn main() {
let s = String::from("hello");

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

De la même manière, si votre slice contient le dernier octet de la String, vous pouvez ne rien mettre à la fin. Cela veut dire que ces deux cas sont identiques :


#![allow(unused)]
fn main() {
let s = String::from("hello");

let taille = s.len();

let slice = &s[3..taille];
let slice = &s[3..];
}

Vous pouvez aussi ne mettre aucune limite pour créer une slice de toute la chaîne de caractères. Ces deux cas sont donc identiques :


#![allow(unused)]
fn main() {
let s = String::from("hello");

let taille = s.len();

let slice = &s[0..taille];
let slice = &s[..];
}

Remarque : Les indices de l'intervalle d'une slice de chaîne doivent toujours se trouver dans les zones acceptables de séparation des caractères encodés en UTF-8. Si vous essayez de créer une slice de chaîne qui s'arrête au milieu d'un caractère encodé sur plusieurs octets, votre programme va se fermer avec une erreur. Afin de simplifier l'explication des slices de chaînes, nous utiliserons uniquement l'ASCII dans cette section ; nous verrons la gestion d'UTF-8 dans la section “Stocker du texte encodé en UTF-8 avec les chaînes de caractères” du chapitre 8.

Maintenant que nous savons tout cela, essayons de réécrire premier_mot pour qu'il retourne une slice. Le type pour les slices de chaînes de caractères s'écrit &str :

Fichier : src/main.rs

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

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

    &s[..]
}

fn main() {}

Nous récupérons l'indice de la fin du mot de la même façon que nous l'avions fait dans l'encart 4-7, en cherchant la première occurrence d'une espace. Lorsque nous trouvons une espace, nous retournons une slice de chaîne en utilisant le début de la chaîne de caractères et l'indice de l'espace comme indices de début et de fin respectivement.

Désormais, quand nous appelons premier_mot, nous récupérons une unique valeur qui est liée à la donnée de base. La valeur se compose d'une référence vers le point de départ de la slice et du nombre d'éléments dans la slice.

Retourner une slice fonctionnerait aussi pour une fonction second_mot :

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

Nous avons maintenant une API simple qui est bien plus difficile à mal utiliser, puisque le compilateur va s'assurer que les références dans la String seront toujours en vigueur. Vous souvenez-vous du bogue du programme de l'encart 4-8, lorsque nous avions un indice vers la fin du premier mot mais qu'ensuite nous avions vidé la chaîne de caractères et que notre indice n'était plus valide ? Ce code était logiquement incorrect, mais ne montrait pas immédiatement une erreur. Les problèmes apparaîtront plus tard si nous essayons d'utiliser l'indice du premier mot avec une chaîne de caractères qui a été vidée. Les slices rendent ce bogue impossible et nous signalent bien plus tôt que nous avons un problème avec notre code. Utiliser la version avec la slice de premier_mot va causer une erreur de compilation :

Fichier : src/main.rs

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

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

    &s[..]
}

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

    let mot = premier_mot(&s);

    s.clear(); // Erreur !

    println!("Le premier mot est : {}", mot);
}

Voici l'erreur du compilateur :

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let mot = premier_mot(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // Erreur !
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("Le premier mot est : {}", mot);
   |                                         --- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Rappelons-nous que d'après les règles d'emprunt, si nous avons une référence immuable vers quelque chose, nous ne pouvons pas avoir une référence mutable en même temps. Étant donné que clear a besoin de modifier la String, il a besoin d'une référence mutable. Le println! qui a lieu après l'appel à clear utilise la référence à mot, donc la référence immuable sera toujours en vigueur à cet endroit. Rust interdit la référence mutable dans clear et la référence immuable pour mot au même moment, et la compilation échoue. Non seulement Rust a simplifié l'utilisation de notre API, mais il a aussi éliminé une catégorie entière d'erreurs au moment de la compilation !

Les littéraux de chaîne de caractères sont aussi des slices

Rappelez-vous lorsque nous avons appris que les littéraux de chaîne de caractères étaient enregistrés dans le binaire. Maintenant que nous connaissons les slices, nous pouvons désormais comprendre les littéraux de chaîne.


#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Ici, le type de s est un &str : c'est une slice qui pointe vers un endroit précis du binaire. C'est aussi la raison pour laquelle les littéraux de chaîne sont immuables ; &str est une référence immuable.

Les slices de chaînes de caractères en paramètres

Savoir que l'on peut utiliser des slices de littéraux et de String nous incite à apporter une petite amélioration à premier_mot, dont voici la signature :

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

Un Rustacé plus expérimenté écrirait plutôt la signature de l'encart 4-9, car cela nous permet d'utiliser la même fonction sur les &String et aussi les &str :

fn premier_mot(s: &str) -> &str {
    let octets = s.as_bytes();

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

    &s[..]
}

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

    // `premier_mot` fonctionne avec les slices de `String`, que ce soit sur
    // une partie ou sur sur son intégralité
    let mot = premier_mot(&ma_string[0..6]);
    let mot = premier_mot(&ma_string[..]);

    // `premier_mot` fonctionne également sur des références vers des `String`,
    // qui sont équivalentes à des slices de toute la `String`
    let mot = premier_mot(&ma_string);

    let mon_litteral_de_chaine = "hello world";

    // `premier_mot` fonctionne avec les slices de littéraux de chaîne, qu'elles
    // soient partielles ou intégrales
    let mot = premier_mot(&mon_litteral_de_chaine[0..6]);
    let mot = premier_mot(&mon_litteral_de_chaine[..]);

    // Comme les littéraux de chaîne *sont* déjà des slices de chaînes,
    // cela fonctionne aussi, sans la syntaxe de slice !
    let mot = premier_mot(mon_litteral_de_chaine);
}

Encart 4-9 : Amélioration de la fonction premier_mot en utilisant une slice de chaîne de caractères comme type du paramètre s

Si nous avons une slice de chaîne, nous pouvons la passer en argument directement. Si nous avons une String, nous pouvons envoyer une référence ou une slice de la String. Cette flexibilité nous est offerte par l'extrapolation de déréferencement, une fonctionnalité que nous allons découvrir dans une section du Chapitre 15. Définir une fonction qui prend une slice de chaîne plutôt qu'une référence à une String rend notre API plus générique et plus utile sans perdre aucune fonctionnalité :

Fichier : src/main.rs

fn premier_mot(s: &str) -> &str {
    let octets = s.as_bytes();

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

    &s[..]
}

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

    // `premier_mot` fonctionne avec les slices de `String`, que ce soit sur
    // une partie ou sur sur son intégralité
    let mot = premier_mot(&ma_string[0..6]);
    let mot = premier_mot(&ma_string[..]);

    // `premier_mot` fonctionne également sur des références vers des `String`,
    // qui sont équivalentes à des slices de toute la `String`
    let mot = premier_mot(&ma_string);

    let mon_litteral_de_chaine = "hello world";

    // `premier_mot` fonctionne avec les slices de littéraux de chaîne, qu'elles
    // soient partielles ou intégrales
    let mot = premier_mot(&mon_litteral_de_chaine[0..6]);
    let mot = premier_mot(&mon_litteral_de_chaine[..]);

    // Comme les littéraux de chaîne *sont* déjà des slices de chaînes,
    // cela fonctionne aussi, sans la syntaxe de slice !
    let mot = premier_mot(mon_litteral_de_chaine);
}

Les autres slices

Les slices de chaînes de caractères, comme vous pouvez l'imaginer, sont spécifiques aux chaînes de caractères. Mais il existe aussi un type de slice plus générique. Imaginons ce tableau de données :


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Tout comme nous pouvons nous référer à une partie d'une chaîne de caractères, nous pouvons nous référer à une partie d'un tableau. Nous pouvons le faire comme ceci :


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

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

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

Cette slice est de type &[i32]. Elle fonctionne de la même manière que les slices de chaînes de caractères, en enregistrant une référence vers le premier élément et une longueur. Vous utiliserez ce type de slice pour tous les autres types de collections. Nous aborderons ces collections en détail quand nous verrons les vecteurs au chapitre 8.

Résumé

Les concepts de possession, d'emprunt et de slices garantissent la sécurité de la mémoire dans les programmes Rust au moment de la compilation. Le langage Rust vous donne le contrôle sur l'utilisation de la mémoire comme tous les autres langages de programmation système, mais le fait que celui qui possède des données nettoie automatiquement ces données quand il sort de la portée vous permet de ne pas avoir à écrire et déboguer du code en plus pour avoir cette fonctionnalité.

La possession influe sur de nombreuses autres fonctionnalités de Rust, c'est pourquoi nous allons encore parler de ces concepts plus loin dans le livre. Passons maintenant au chapitre 5 et découvrons comment regrouper des données ensemble dans une struct.