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() {}
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 ! }
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.
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);
}
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
.