Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Le type slice

Les slices vous permettent de faire référence à une séquence contiguë d’éléments dans une collection, plutôt qu’à la collection entière. Une slice est un type de référence, et donc ne possède pas sa valeur.

Voici un petit problème de programmation : écrivez 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, la chaîne entière doit être un seul mot, donc la chaîne entière doit être renvoyée.

Note : Pour les besoins de l’introduction des slices, nous supposons uniquement de l’ASCII dans cette section ; une discussion plus approfondie du traitement de l’UTF-8 se trouve dans la section [“Stocker du texte encode en UTF-8 avec les Strings”][strings] du chapitre 8.

Voyons comment nous écririons la signature de cette fonction sans utiliser de slices, pour comprendre le problème que les slices vont résoudre :

fn first_word(s: &String) -> ?

La fonction first_word à un paramètre de type &String. Nous n’avons pas besoin de la possession, donc c’est bien. (En Rust idiomatique, les fonctions ne prennent pas la possession de leurs arguments a moins qu’elles n’en aient besoin, et les raisons deviendront claires au fur et a mesure.) 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’indice de la fin du mot, indique par un espace. Essayons cela, comme le montre le listing 4-7.

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {}
Listing 4-7: The first_word function that returns a byte index value into the String parameter

Comme nous devons parcourir la String élément par élément et vérifier si une valeur est un espace, nous convertirons notre String en un tableau d’octets en utilisant la methode as_bytes. rust,ignore {{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:as_bytes}}

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

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

    s.len()
}

fn main() {}

Ensuite, nous créons un iterateur sur le tableau d’octets en utilisant la methode iter : rust,ignore {{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:iter}}

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

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

    s.len()
}

fn main() {}

Nous discuterons des itérateurs plus 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 de partie d’un tuple à la place. 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. C’est un peu plus pratique que de calculer l’index nous-mêmes.

Comme la methode enumerate renvoie un tuple, nous pouvons utiliser des motifs (patterns) pour destructurer ce tuple. Nous discuterons des motifs plus en détail au [chapitre 6][ch6]. Dans la boucle for, nous specif ions un motif qui a i pour l’indice dans le tuple et &item pour l’octet unique dans le tuple. Comme nous obtenons une référence vers l’élément depuis .iter().enumerate(), nous utilisons & dans le motif.

A l’intérieur 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 un espace, nous renvoyons la position. Sinon, nous renvoyons la longueur de la chaîne en utilisant s.len(). rust,ignore {{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:inside_for}}

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

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

    s.len()
}

fn main() {}

Nous avons maintenant un moyen de trouver l’indice de la fin du premier mot dans la chaîne, mais il y à un problème. Nous renvoyons un usize seul, mais c’est un nombre qui n’a de sens que dans le contexte de la &String. En d’autres termes, comme c’est une valeur séparée de la String, il n’y à aucune garantie qu’elle sera encore valide à l’avenir. Considérez le programme du listing 4-8 qui utilise la fonction first_word du listing 4-7.

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

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

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but s no longer has any content that we
    // could meaningfully use with the value 5, so word is now totally invalid!
}
Listing 4-8: Storing the result from calling the first_word function and then changing the String contents

Ce programme compilé sans aucune erreur et le ferait également si nous utilisions word après avoir appelé s.clear(). Comme word n’est pas du tout connecte à 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 bug car le contenu de s a change depuis que nous avons enregistré 5 dans word.

Devoir s’inquieter de la desynchronisation de l’indice dans word avec les données dans s est fastidieux et source d’erreurs ! La gestion de ces indices est encore plus fragile si nous écrivons une fonction second_word. Sa signature devrait ressembler a ceci :

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

Maintenant nous suivons un indice de début et un indice de fin, et nous avons encore plus de valeurs qui ont été calculees à partir de données dans un état particulier mais qui ne sont pas du tout liees à cet état. Nous avons trois variables non liees qui flottent et qui doivent être maintenues synchronisees.

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

Les slices de chaînes de caractères

Une slice de chaine est une référence à une séquence contigue des éléments d’une String, et elle ressemble a ceci : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-17-slice/src/main.rs:here}}

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

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

Plutot qu’une référence à la String entière, hello est une référence à une portion de la String, spécifiée par la partie supplementaire [0..5]. Nous créons des slices en utilisant un intervalle entre crochets en specifiant [indice_de_debut..indice_de_fin], ou indice_de_debut est la première position dans la slice et indice_de_fin est un de plus que la derniere position dans la slice. En interne, la structure de données de la slice stocké la position de début et la longueur de la slice, qui correspond a indice_de_fin moins indice_de_debut. Ainsi, dans le cas de let world = &s[6..11];, world serait une slice qui contient un pointeur vers l’octet à l’indice 6 de s avec une valeur de longueur de 5.

La figure 4-7 montre cela dans un diagramme.

Trois tableaux : un tableau representant les donnees de la pile de s, qui pointe vers l’octet a l’indice 0 dans un tableau des donnees de chaine "hello world" sur le tas. Le troisieme tableau represente les donnees de la pile de la slice world, qui a une valeur de longueur de 5 et pointe vers l’octet 6 du tableau de donnees du tas. Figure 4-7 : Une slice de chaîne faisant référence à une partie d’une `String`

Avec la syntaxe d’intervalle .. de Rust, si vous voulez commencer à l’indice 0, vous pouvez omettre la valeur avant les deux points. En d’autres termes, ceux-ci sont equivalents :

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

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

De la même façon, si votre slice inclut le dernier octet de la String, vous pouvez omettre le nombre final. Cela signifie que ceux-ci sont equivalents :

#![allow(unused)]
fn main() {
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 slice de la chaîne entière. Ainsi, ceux-ci sont equivalents :

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

let len = s.len();

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

Note : Les indices d’intervalle des slices de chaînes doivent se situer à des limites de caractères UTF-8 valides. Si vous tentez de créer une slice de chaîne au milieu d’un caractère multi-octets, votre programme se terminera avec une erreur.

Avec toutes ces informations en tete, réécrivons first_word pour renvoyer une slice. Le type qui designe “slice de chaîne” s’écrit &str :

Filename: 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[..]
}

fn main() {}

Nous obtenons l’indice de la fin du mot de la même façon que dans le listing 4-7, en recherchant la première occurrence d’un espace. Quand nous trouvons un espace, nous renvoyons une slice de chaîne en utilisant le début de la chaîne et l’indice de l’espace comme indices de début et de fin.

Maintenant, quand nous appelons first_word, nous recuperons une seule valeur qui est liee aux données sous-jacentes. La valeur est composee d’une référence au point de depart de la slice et du nombre d’éléments dans la slice.

Renvoyer une slice fonctionnerait également pour une fonction second_word :

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

Nous avons maintenant une API simple qui est beaucoup plus difficile a mal utiliser car le compilateur s’assurera que les références dans la String restent valides. Rappelez-vous le bug dans le programme du listing 4-8, quand nous avons obtenu l’indice de la fin du premier mot mais avons ensuite vide la chaîne, rendant notre indice invalide ? Ce code était logiquement incorrect mais ne montrait aucune erreur immediate. Les problèmes apparaîtraient plus tard si nous continuions a essayer d’utiliser l’indice du premier mot avec une chaîne videe. Les slices rendent ce bug impossible et nous informent bien plus tot que nous avons un problème avec notre code. Utiliser la version avec slice de first_word produira une erreur de compilation :

Filename: 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[..]
}

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

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

Voici l’erreur du compilateur : console {{#include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/output.txt}}

$ 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 word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here

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

Rappelez-vous des règles d’emprunt que si nous avons une référence immuable vers quelque chose, nous ne pouvons pas également prendre une référence mutable. Comme clear a besoin de tronquer la String, elle a besoin d’obtenir une référence mutable. Le println! après l’appel a clear utilise la référence dans word, donc la référence immuable doit encore être activé à ce moment-la. 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 echoue. Non seulement Rust a rendu notre API plus facile à utiliser, mais il a aussi elimine toute une classe d’erreurs à la compilation !

Les littéraux de chaînes comme slices

Rappelez-vous que nous avons parle des littéraux de chaînes stockés à l’intérieur du binaire. Maintenant que nous connaissons les slices, nous pouvons comprendre correctement les littéraux de chaînes :

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

Le type de s ici est &str : c’est une slice pointant vers ce point spécifique du binaire. C’est aussi pourquoi les littéraux de chaînes sont immuables ; &str est une référence immuable.

Les slices de chaînes comme paramètres

Savoir que vous pouvez prendre des slices de littéraux et de valeurs String nous amene à une amélioration supplementaire de first_word, et c’est sa signature :

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

Un Rustacean plus experimente écrirait plutot la signature montrée dans le listing 4-9 car elle nous permet d’utiliser la même fonction sur les valeurs &String et les valeurs &str.

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

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

    &s[..]
}

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

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on références to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 4-9: Improving the first_word function by using a string slice for the type of the s parameter

Si nous avons une slice de chaîne, nous pouvons la passer directement. Si nous avons une String, nous pouvons passer une slice de la String ou une référence à la String. Cette flexibilite tire parti des conversions automatiques de dereferencement (deref coercions), une fonctionnalité que nous couvrirons dans la section [“Utiliser les conversions automatiques de dereferencement dans les fonctions et les methodes”][deref-coercions] du chapitre 15.

Définir une fonction pour prendre une slice de chaîne au lieu d’une référence à une String rend notre API plus generale et utile sans perdre de fonctionnalité :

Filename: src/main.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

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

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on références to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Les autres slices

Les slices de chaînes, comme vous pouvez l’imaginer, sont spécifiques aux chaînes. Mais il existe aussi un type de slice plus general. Considérez ce tableau :

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

Tout comme nous pourrions vouloir faire référence à une partie d’une chaîne, nous pourrions vouloir faire référence à une partie d’un tableau. Nous le ferions 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 à le type &[i32]. Elle fonctionne de la même maniere que les slices de chaînes, en stockant une référence vers le premier élément et une longueur. Vous utiliserez ce type de slice pour toutes sortes d’autres collections. Nous discuterons de ces collections en détail quand nous parlerons des vecteurs au chapitre 8.

Résumé

Les concepts de possession, d’emprunt et de slices garantissent la securite de la mémoire dans les programmes Rust à la compilation. Le langage Rust vous donne le contrôle sur votre utilisation de la mémoire de la même façon que les autres langages de programmation système. Mais le fait que le propriétaire des données nettoie automatiquement ces données quand le propriétaire sort de la portée signifie que vous n’avez pas a écrire et deboguer du code supplementaire pour obtenir ce contrôle.

La possession affecte le fonctionnement de nombreuses autres parties de Rust, nous continuerons donc a parler de ces concepts tout au long du reste du livre. Passons au chapitre 5 et voyons comment regrouper des données dans une struct.