Aller au contenu

Les slices en Rust

Une slice (tranche) vous permet d’emprunter une partie d’un tableau ou d’un vecteur, une sorte de « fenêtre » sur une portion des données, sans copier ces données ni en prendre l’ownership (possession). Si vous avez déjà utilisé le borrowing (emprunt) avec une seule valeur, une slice, c’est la même idée appliquée à une plage d’éléments.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // les éléments aux indices 1, 2 et 3
println!("{:?}", middle);
}

Sortie :

[20, 30, 40]
▶ Essayez par vous-même

1..4 est le même genre de plage que celle des boucles for : elle commence à l’indice 1 et s’arrête avant l’indice 4. Vous obtenez donc les indices 1, 2 et 3.

Quelques raccourcis que vous croiserez souvent :

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // du début jusqu'à l'indice 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // de l'indice 2 jusqu'à la fin -> [30, 40, 50]
println!("{:?}", &numbers[..]); // le tout -> [10, 20, 30, 40, 50]
}
▶ Essayez par vous-même

En fait, vous utilisez déjà des slices : &str est une slice d’une String (ou d’une autre chaîne) ! C’est pour ça que les deux types fonctionnent si bien ensemble :

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ Essayez par vous-même

La phrase reste en anglais exprès : les indices d’une slice de texte comptent des octets, et une lettre accentuée comme « é » en occupe deux. Avec une phrase en français, 0..3 ne tomberait pas forcément sur la fin d’un mot.

On pourrait imaginer résoudre le problème en copiant la portion voulue dans un nouveau Vec ou une nouvelle String. Les slices évitent complètement cela : elles pointent simplement vers une partie des données existantes, de la même façon qu’une référence (&) pointe vers une seule valeur. Résultat : aucune allocation en plus, aucune copie, et les règles d’emprunt que vous connaissez déjà grâce à la page Borrowing s’appliquent toujours.

Les slices sont particulièrement utiles comme paramètres de fonction, car elles acceptent à la fois les tableaux et les vecteurs :

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Essayez par vous-même

&[i32] accepte une slice venant aussi bien d’un tableau que d’un Vec : une seule fonction, et les deux appels fonctionnent.

Jusqu’ici, nos slices se contentaient de lire les données. Une slice mutable, &mut [T], permet à une fonction de modifier les éléments qu’on lui a donnés, et seulement ceux-là :

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // seulement les indices 1 et 2
println!("{:?}", scores);
double_all(&mut scores); // le vecteur entier
println!("{:?}", scores);
}

Sortie :

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Essayez par vous-même

Le premier appel passe &mut scores[1..3], donc double_all ne voit que les indices 1 et 2. Tout ce qui se trouve en dehors de cette plage est hors de sa portée. À l’intérieur de la fonction, la slice recommence à l’indice 0 : pour double_all, c’est simplement une liste de deux nombres.

C’est sûr grâce à une règle que vous verrez en détail sur la page Borrowing : tant que la fonction détient une slice &mut, rien d’autre ne peut utiliser ces éléments. Deux parties de votre code ne peuvent donc jamais les modifier en même temps. Le vecteur doit aussi être déclaré mut, sinon Rust refuse de l’emprunter de façon mutable.

Créez le vecteur vec![3, 6, 9, 12, 15, 18]. Affichez sa première moitié et sa seconde moitié sous forme de deux slices séparées. Indice : calculez let mid = numbers.len() / 2; et utilisez ..mid et mid...

Voir la solution
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Première moitié : {:?}", first_half);
println!("Seconde moitié : {:?}", second_half);
}

Sortie :

Première moitié : [3, 6, 9]
Seconde moitié : [12, 15, 18]
▶ Essayez par vous-même

Comme mid est calculé à partir de len(), ce code continue de fonctionner si vous ajoutez ou retirez des nombres. Avec une longueur impaire, l’élément en trop se retrouve dans la seconde moitié.

Écrivez une fonction largest(numbers: &[i32]) -> i32 qui renvoie le plus grand nombre. Appelez-la avec un tableau, avec un vecteur, et avec une slice qui ne contient que le premier élément du vecteur. Indice : partez de numbers[0] et parcourez le reste avec une boucle, comme vous le feriez avec un tableau.

Voir la solution
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Sortie :

9
35
10
▶ Essayez par vous-même

Parcourir une slice avec une boucle vous donne une référence vers chaque élément (&i32), pas le nombre lui-même. *n signifie « la valeur vers laquelle pointe n », donc *n > biggest compare deux nombres ordinaires. Cette version provoque un panic sur une slice vide, car numbers[0] n’existe pas. Plus tard, vous apprendrez à renvoyer une Option dans ce cas.

  • Une slice (&[T]) emprunte une partie d’un tableau ou d’un vecteur sans la copier.
  • 1..4 sélectionne les indices 1 à 3 ; omettez un côté de .. pour partir du début ou aller jusqu’à la fin.
  • &str est lui-même une slice de texte : c’est pour ça que les chaînes et les slices se comportent de façon si proche.
  • Préférez &[T] comme type de paramètre quand vous voulez accepter à la fois des tableaux et des vecteurs.
  • Une slice mutable (&mut [T]) permet à une fonction de modifier uniquement les éléments qu’on lui a donnés.

Petit quiz

1. Que permet une slice avec une partie d'un tableau ou d'un vecteur ?

2. Quels indices &numbers[1..4] inclut-il ?

3. Pourquoi &[i32] est-il un bon choix comme type de paramètre de fonction ?

Score : 0 / 3