
Les tableaux (arrays) : collections de taille fixe
Apprenez à utiliser les tableaux (arrays) en Rust, des collections de taille fixe où tous les éléments doivent être du même type. Idéal pour les données stockées sur la pile.
Les tableaux : des listes ordonnées et immuables en taille
Après les tuples qui regroupent des valeurs de types potentiellement différents, nous abordons un autre type de collection fondamental en Rust : le tableau (array). Un tableau est une collection d'éléments qui doivent tous être du même type. Une caractéristique distinctive et cruciale des tableaux en Rust est qu'ils ont une taille fixe : cette taille est connue au moment de la compilation et ne peut pas être modifiée pendant l'exécution du programme.
Ce sous-chapitre vous expliquera comment déclarer, initialiser et accéder aux éléments des tableaux. Nous mettrons en lumière les avantages des tableaux, notamment leur allocation sur la pile qui peut offrir des performances supérieures pour certaines applications, ainsi que les garanties de sécurité que Rust fournit lors de l'accès à leurs éléments.
Bien que leur taille fixe les rende moins flexibles que d'autres types de collections comme les vecteurs (que nous verrons plus tard), les tableaux sont un outil précieux lorsque le nombre d'éléments est connu à l'avance et ne change pas, par exemple pour représenter les jours de la semaine, les coordonnées d'un point dans un espace à N dimensions fixes, ou pour des tampons de données de taille prédéfinie.
Déclaration et initialisation des tableaux
En Rust, le type d'un tableau est spécifié par le type de ses éléments et sa taille, sous la forme `[T; N]`, où `T` est le type des éléments et `N` est le nombre d'éléments (la taille). La taille `N` doit être une constante entière positive.
Vous pouvez initialiser un tableau en fournissant une liste de valeurs entre crochets, séparées par des virgules. Le compilateur peut souvent inférer le type et la taille si vous ne les spécifiez pas explicitement.
fn main() {
// Déclaration avec type et taille explicites
let nombres: [i32; 5] = [1, 2, 3, 4, 5];
// Le compilateur infère le type [i32; 3]
let scores = [100, 95, 88];
// Tableau de chaînes de caractères (tranches de chaînes, &str)
let jours: [&str; 7] = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"];
println!("Premier nombre : {}", nombres[0]);
println!("Score de milieu : {}", scores[1]);
println!("Troisième jour : {}", jours[2]);
}Il existe une syntaxe pratique si vous souhaitez initialiser un tableau où chaque élément a la même valeur. Vous pouvez spécifier la valeur initiale, suivie d'un point-virgule, puis de la taille du tableau.
Exemple d'initialisation avec une valeur répétée :
fn main() {
// Un tableau de 10 entiers, tous initialisés à 0
let buffer_zeros: [u8; 10] = [0; 10];
// Equivalent à: let buffer_zeros = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// Un tableau de 5 booléens, tous initialisés à true
let flags = [true; 5];
println!("Buffer (premier élément) : {}", buffer_zeros[0]);
println!("Flags (dernier élément) : {}", flags[4]);
// Pour afficher tout le tableau (nécessite que les éléments implémentent Debug)
println!("Buffer complet : {:?}", buffer_zeros);
println!("Flags complets : {:?}", flags);
}Cette syntaxe `[valeur_initiale; taille]` est très utile pour créer rapidement des tableaux remplis d'une valeur par défaut.
Les données d'un tableau sont stockées en mémoire de manière contiguë. Si le type des éléments du tableau est stockable sur la pile (comme les types scalaires ou les tuples/tableaux de types stockables sur la pile), alors le tableau entier sera alloué sur la pile. C'est un avantage en termes de performance par rapport aux collections allouées sur le tas, car l'accès aux éléments est très rapide et l'allocation/désallocation est quasi instantanée.
Accéder aux éléments d'un tableau et sécurité des accès
Vous accédez aux éléments d'un tableau en utilisant une indexation entre crochets, tout comme dans de nombreux autres langages de programmation. L'indexation en Rust commence à 0. Ainsi, pour un tableau de taille `N`, les indices valides vont de `0` à `N-1`.
Exemple d'accès aux éléments :
fn main() {
let mon_tableau = [10, 20, 30, 40, 50];
let premier = mon_tableau[0]; // Accède au premier élément (10)
let troisieme = mon_tableau[2]; // Accède au troisième élément (30)
println!("Premier élément : {}", premier);
println!("Troisième élément : {}", troisieme);
// On peut aussi modifier un élément si le tableau est mutable
let mut tableau_mutable = [1, 2, 3];
tableau_mutable[1] = 200; // Modifie le deuxième élément
println!("Tableau mutable modifié : {:?}", tableau_mutable);
}Rust accorde une grande importance à la sécurité de la mémoire. L'une des erreurs courantes dans des langages comme C ou C++ est l'accès à un tableau en dehors de ses limites valides (buffer overflow). Rust prévient ce type d'erreur à l'exécution. Si vous essayez d'accéder à un élément d'un tableau en utilisant un index qui est en dehors de la plage valide, votre programme entrera en panic. Un panic est un arrêt non récupérable du thread courant, accompagné d'un message d'erreur.
Exemple d'accès hors limites (qui paniquerait) :
fn main() {
let petit_tableau = [100, 200];
// L'instruction suivante provoquerait un panic à l'exécution car l'index 2 est hors limites
// pour un tableau de taille 2 (indices valides : 0, 1).
// let element_invalide = petit_tableau[2];
// println!("Elément invalide : {}", element_invalide);
// Pour éviter un panic, vous pouvez vérifier la longueur avant d'accéder
// ou utiliser des méthodes plus sûres comme .get()
let index_demande = 1;
if index_demande < petit_tableau.len() {
println!("Elément à l'index {} : {}", index_demande, petit_tableau[index_demande]);
} else {
println!("Index {} hors limites.", index_demande);
}
// La méthode .get() retourne un Option<&T>
match petit_tableau.get(1) {
Some(valeur) => println!("Valeur obtenue avec .get() : {}", valeur),
None => println!("Index invalide avec .get()."),
}
match petit_tableau.get(5) {
Some(valeur) => println!("Valeur obtenue avec .get() : {}", valeur), // Ne sera pas atteint
None => println!("Index 5 invalide avec .get()."), // Sera affiché
}
}La méthode `get(index)` sur un tableau (ou une slice) est une alternative plus sûre à l'indexation directe `[]`. Elle retourne un `Option<&T>` : `Some(&element)` si l'index est valide, ou `None` si l'index est hors limites. Cela vous permet de gérer l'éventualité d'un index invalide de manière programmatique sans provoquer de panic.
La propriété `len()` permet d'obtenir la taille (longueur) d'un tableau. Etant donné que la taille est fixée à la compilation, `len()` retourne toujours la même valeur pour un tableau donné.
Quand utiliser les tableaux ?
Les tableaux sont les plus utiles lorsque vous savez que le nombre d'éléments de votre collection ne changera pas. Par exemple :
- Les mois de l'année : `["Janvier", ..., "Décembre"]` (un tableau de `[&str; 12]`).
- Les jours de la semaine.
- Les couleurs primaires RGB : `[255, 0, 0]` (un tableau de `[u8; 3]`).
- Des données de configuration fixes.
- Des tampons de petite taille pour des opérations d'entrée/sortie où la taille maximale est connue.
Leur allocation sur la pile les rend très performants pour des petites collections. Cependant, si vous avez besoin d'une collection qui peut grandir ou rétrécir dynamiquement, les tableaux ne sont pas le bon choix. Dans ce cas, Rust propose le type `Vec<T>` (vecteur), qui est une collection de taille variable allouée sur le tas. Les vecteurs sont beaucoup plus flexibles mais peuvent avoir un léger surcoût de performance dû à l'allocation sur le tas et à la gestion de la capacité.
En résumé, les tableaux sont une structure de données simple et efficace pour des ensembles de données de taille fixe et homogènes. Leur sécurité d'accès garantie par Rust et leur potentiel de performance grâce à l'allocation sur la pile en font un outil important dans la boîte à outils du développeur Rust.