Chapitre 06
Collections : Vec, tableaux, slices
Objectifs du chapitre
- Distinguer tableaux Ă taille fixe
[T; N],Vec<T>et slices&[T], et savoir quand utiliser chacun - Comprendre le coût des allocations/réallocations d'un
Vecet l'intĂ©rĂȘt dewith_capacityen contexte temps rĂ©el - Ăcrire des fonctions gĂ©nĂ©riques prenant des slices en paramĂštre
- Choisir entre indexation
v[i](panic) etv.get(i)(Option) selon le contexte - Utiliser les itérateurs de base (
iter,enumerate,map,sum,zip) pour manipuler des vecteurs d'état
1. Tableaux Ă taille fixe [T; N]
Un tableau Rust [T; N] est l'équivalent direct d'un tableau C T tab[N]; :
sa taille N fait partie du type, elle est connue Ă la compilation, et le tableau vit
entiÚrement sur la pile (aucune allocation dynamique). C'est le choix par défaut en contexte
temps rĂ©el dĂšs que la dimension est fixe â un Ă©tat de robot Ă 3 degrĂ©s de libertĂ©, un buffer
de mesures IMU Ă taille constante, etc.
fn main() {
// Un état [x, y, theta] : position + orientation d'un robot mobile.
let etat: [f64; 3] = [0.0, 0.0, 0.0];
// Initialisation répétée d'une valeur : [valeur; N]
let zeros: [f64; 5] = [0.0; 5];
println!("etat = {:?}", etat);
println!("taille = {} éléments, {} octets",
etat.len(), std::mem::size_of_val(&etat));
// AccÚs par index : identique au C, mais vérifié à l'exécution.
let x = etat[0];
println!("x = {x}");
}
Comparaison avec C : en C, int tab[3]; ne connaĂźt pas sa propre taille une fois
passĂ© Ă une fonction (il « decay » en pointeur) â il faut la transmettre Ă part. En Rust, le
type [f64; 3] garde sa taille dans la signature de la fonction, et le compilateur
vérifie tout à la compilation. Il n'y a ni allocation, ni pointeur caché : la disposition
mémoire est identique à celle du tableau C, avec en plus les vérifications de bornes.
Un tableau fixe implémente Copy si son type d'éléments l'implémente (c'est le
cas de f64). L'affecter Ă une autre variable en fait donc une copie complĂšte,
comme en C â pas de dĂ©placement (move) ni d'emprunt implicite.
2. Vec<T> : le tableau dynamique
Vec<T> est l'équivalent de std::vector<T> en C++ : une
structure de trois champs (pointeur vers le tas, longueur, capacitĂ©) qui gĂšre elle-mĂȘme son
allocation. Utile quand la taille n'est pas connue Ă la compilation â une trajectoire chargĂ©e
depuis un fichier, une liste d'obstacles détectés, etc.
fn main() {
let mut mesures: Vec<f64> = Vec::new();
mesures.push(1.2);
mesures.push(3.4);
mesures.push(-0.8);
println!("len = {}, capacity = {}", mesures.len(), mesures.capacity());
let dernier = mesures.pop(); // Option<f64> : Some(-0.8) ici
println!("dernier retiré : {:?}", dernier);
// Indexation comme un tableau classique.
println!("mesures[0] = {}", mesures[0]);
}
Un Vec commence avec une capacité de 0. Chaque push qui dépasse la
capacité actuelle déclenche une réallocation : le runtime alloue un nouveau
bloc mémoire (typiquement le double de la taille précédente), copie tous les éléments existants,
puis libĂšre l'ancien bloc. C'est coĂ»teux et surtout imprĂ©visible en durĂ©e â problĂ©matique
dans une boucle de contrÎle temps réel. Si la taille finale est connue à l'avance, on la réserve
d'un coup :
fn main() {
// Réserve la capacité pour 100 éléments en une seule allocation.
let mut trajectoire: Vec<f64> = Vec::with_capacity(100);
for i in 0..100 {
trajectoire.push(i as f64 * 0.01);
}
// Aucune réallocation n'a eu lieu pendant la boucle : capacity >= 100 dÚs le départ.
println!("len = {}, capacity = {}", trajectoire.len(), trajectoire.capacity());
}
Dans une boucle de contrÎle qui tourne à fréquence fixe (ex. 1 kHz), éviter toute
allocation : préférer un tableau fixe [f64; N] quand la taille est
connue, ou un Vec créé une seule fois avant la boucle avec
Vec::with_capacity puis réutilisé (clear() + push,
ou écriture directe par index) plutÎt que recréé à chaque itération. Une allocation/désallocation
dans le chemin chaud introduit une latence non déterministe qui peut casser une échéance temps réel.
3. Slices &[T] et &mut [T]
Une slice est une vue empruntĂ©e sur une sĂ©quence contiguĂ« d'Ă©lĂ©ments â un
pointeur vers le début plus une longueur, sans posséder les données ni les copier. On peut
obtenir une slice aussi bien Ă partir d'un tableau fixe que d'un Vec, ce qui en
fait le type idéal pour écrire des fonctions génériques indépendantes du conteneur d'origine.
// Fonction générique : accepte un tableau fixe, un Vec, ou une autre slice.
fn norme(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn mettre_a_zero(v: &mut [f64]) {
for x in v.iter_mut() {
*x = 0.0;
}
}
fn main() {
let etat_fixe: [f64; 3] = [3.0, 4.0, 0.0];
let mut etat_vec: Vec<f64> = vec![3.0, 4.0, 0.0];
// &etat_fixe et &etat_vec[..] se convertissent tous deux en &[f64]
println!("norme (tableau) = {}", norme(&etat_fixe));
println!("norme (Vec) = {}", norme(&etat_vec));
mettre_a_zero(&mut etat_vec);
println!("aprÚs remise à zéro : {:?}", etat_vec);
// Sous-slice : les 2 premiers éléments seulement.
let debut = &etat_fixe[0..2];
println!("debut = {:?}", debut);
}
La rĂšgle de conception Ă retenir : quand une fonction n'a besoin que de lire ou
modifier des éléments sans changer la taille de la collection, on la fait accepter
une slice (&[T] ou &mut [T]) plutĂŽt qu'un &Vec<T>
ou un &[T; N] précis. C'est plus flexible pour l'appelant, sans coût : aucune
copie n'a lieu, la slice est juste un pointeur + une longueur passés par valeur.
4. AccÚs sûr : indexation vs .get()
En C/C++, lire tab[i] avec un i hors bornes est un comportement
indéfini : le programme peut lire n'importe quelle mémoire adjacente, silencieusement, et planter
bien plus tard sans lien apparent avec la cause réelle. En Rust, l'indexation v[i]
est vérifiée à l'exécution : si i dépasse la longueur, le programme
panique immédiatement avec un message clair, plutÎt que de corrompre la mémoire
en silence.
fn main() {
let mesures = vec![1.0, 2.0, 3.0];
// Panic contrÎlé si l'index est hors bornes (message explicite, pas de UB).
// let x = mesures[10]; // décommenter -> "index out of bounds: the len is 3 but the index is 10"
// Alternative sûre : .get() renvoie une Option, jamais de panic.
match mesures.get(10) {
Some(x) => println!("valeur = {x}"),
None => println!("index hors bornes, valeur ignorée"),
}
// Idiomatique avec unwrap_or : valeur par défaut si absent.
let x = mesures.get(1).copied().unwrap_or(0.0);
println!("x = {x}");
}
Contrairement au C oĂč un dĂ©passement de tableau peut passer totalement inaperçu (et provoquer
un bug des heures plus tard, ailleurs dans le programme), Rust ne laisse jamais un accĂšs hors
borne silencieux : c'est v[i] qui panique immédiatement, ou v.get(i)
qui force explicitement à gérer le cas d'absence via Option. Dans du code
embarqué critique, on préfÚre souvent .get() (ou une vérification manuelle de
longueur) pour éviter tout panic imprévu en production.
5. Itérateurs de base
Rust encourage à parcourir les collections via des itérateurs plutÎt que des boucles indexées
manuelles â plus lisible, et le compilateur optimise gĂ©nĂ©ralement ce code aussi bien qu'une
boucle C avec index. Un aperçu suffisant pour ce chapitre ; les itérateurs seront approfondis
plus tard (adaptateurs paresseux, collect, performances).
fn main() {
let vitesses: Vec<f64> = vec![0.5, 1.2, 0.8, 2.0];
// Parcours simple par référence : ne consomme pas le Vec.
for v in &vitesses {
print!("{v} ");
}
println!();
// .iter() explicite (équivalent à &vitesses dans un for), avec index via enumerate().
for (i, v) in vitesses.iter().enumerate() {
println!("vitesse[{i}] = {v}");
}
// .iter_mut() pour modifier en place.
let mut copie = vitesses.clone();
for v in copie.iter_mut() {
*v *= 2.0;
}
println!("doublées = {:?}", copie);
// .map() + .sum() : somme des carrés (utile pour une norme).
let somme_carres: f64 = vitesses.iter().map(|v| v * v).sum();
println!("somme des carrés = {somme_carres}");
// .zip() : parcourir deux collections en parallÚle, élément par élément.
// TrÚs utile pour les opérations vectorielles (addition, produit scalaire...).
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let produits: Vec<f64> = a.iter().zip(b.iter()).map(|(x, y)| x * y).collect();
println!("produits terme Ă terme = {:?}", produits);
}
6. Fil rouge : vecteurs d'état, trajectoires, opérations vectorielles
Pour représenter l'état d'un robot, deux choix légitimes selon le contexte :
[f64; N] quand N est fixe et connu (ex. un bras Ă 6 axes,
toujours [f64; 6]), ou Vec<f64> quand la dimension varie
selon la configuration chargĂ©e au dĂ©marrage. Une trajectoire â une suite de points
Vec2 Ă©chantillonnĂ©s dans le temps â se reprĂ©sente naturellement en
Vec<Vec2>, puisque son nombre de points n'est pas connu Ă la compilation.
#[derive(Debug, Clone, Copy)]
struct Vec2 {
x: f64,
y: f64,
}
/// Norme euclidienne d'un vecteur donné sous forme de slice.
/// Accepte aussi bien un [f64; N] qu'un Vec<f64>, grĂące Ă &[f64].
fn norme(v: &[f64]) -> f64 {
v.iter().map(|c| c * c).sum::<f64>().sqrt()
}
/// Produit scalaire de deux vecteurs de mĂȘme longueur, via zip.
fn produit_scalaire(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
/// Addition Ă©lĂ©ment par Ă©lĂ©ment de deux vecteurs de mĂȘme longueur.
fn addition(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}
fn longueur_trajectoire(traj: &[Vec2]) -> f64 {
let mut total = 0.0;
for fenetre in traj.windows(2) {
let dx = fenetre[1].x - fenetre[0].x;
let dy = fenetre[1].y - fenetre[0].y;
total += (dx * dx + dy * dy).sqrt();
}
total
}
fn main() {
// Ătat Ă dimension fixe : bras 6 axes, angles en radians.
let etat_bras: [f64; 6] = [0.0, 0.3, -0.2, 0.0, 1.1, 0.0];
println!("norme de l'état bras = {:.3}", norme(&etat_bras));
// Deux vecteurs vitesse/accélération (dimension dynamique).
let vitesse: Vec<f64> = vec![0.4, -0.1, 0.05];
let acceleration: Vec<f64> = vec![0.02, 0.0, -0.01];
println!("produit scalaire = {}", produit_scalaire(&vitesse, &acceleration));
println!("somme = {:?}", addition(&vitesse, &acceleration));
// Trajectoire : suite de points 2D échantillonnés.
let trajectoire: Vec<Vec2> = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 1.0, y: 1.0 },
];
println!("longueur trajectoire = {:.3}", longueur_trajectoire(&trajectoire));
}
.windows(2) renvoie des slices glissantes de 2 Ă©lĂ©ments consĂ©cutifs â trĂšs
pratique pour calculer des différences entre points successifs d'une trajectoire, sans
gérer manuellement les index i et i-1 comme on le ferait en C.
Exercices
Exercice 1 â Somme et moyenne d'un Vec
Ăcris une fonction moyenne(v: &[f64]) -> f64 qui renvoie la moyenne
arithmétique des éléments d'une slice de f64 (renvoie 0.0 si la
slice est vide, pour éviter une division par zéro). Teste-la avec un Vec<f64>
contenant quelques mesures.
Voir la solution
fn moyenne(v: &[f64]) -> f64 {
if v.is_empty() {
return 0.0;
}
let somme: f64 = v.iter().sum();
somme / v.len() as f64
}
fn main() {
let mesures: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
println!("moyenne = {}", moyenne(&mesures));
let vide: Vec<f64> = Vec::new();
println!("moyenne (vide) = {}", moyenne(&vide));
}
v.iter().sum() additionne tous les éléments de la slice ; le test
is_empty() évite le piÚge classique de la division par zéro sur un
tableau vide, qui en C produirait un comportement indéfini plutÎt qu'un NaN
ou une erreur explicite.
Exercice 2 â Produit scalaire avec .zip()
Sans regarder la section 6, réécris produit_scalaire(a: &[f64], b: &[f64]) -> f64
en utilisant .zip(), .map() et .sum(). Vérifie-la avec
deux slices de longueur 3.
Voir la solution
fn produit_scalaire(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn main() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0];
// 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
println!("produit scalaire = {}", produit_scalaire(&a, &b));
}
zip combine deux itérateurs en un itérateur de paires (x, y),
en s'arrĂȘtant dĂšs que le plus court des deux est Ă©puisĂ© â utile si les slices n'ont pas
exactement la mĂȘme longueur, mĂȘme si dans un vrai calcul vectoriel on vĂ©rifierait
généralement l'égalité des longueurs avant.
Exercice 3 â Trouver le maximum avec .iter()
Ăcris une fonction maximum(v: &[f64]) -> Option<f64> qui renvoie
la plus grande valeur d'une slice, ou None si elle est vide. Indice : les
f64 n'implémentant pas Ord (à cause de NaN),
.max() classique ne fonctionne pas directement â utilise plutĂŽt
.fold() ou f64::max.
Voir la solution
fn maximum(v: &[f64]) -> Option<f64> {
if v.is_empty() {
return None;
}
let mut max = v[0];
for &x in v.iter() {
if x > max {
max = x;
}
}
Some(max)
}
fn main() {
let mesures = [3.0, 7.5, -2.0, 4.1];
println!("max = {:?}", maximum(&mesures));
let vide: [f64; 0] = [];
println!("max (vide) = {:?}", maximum(&vide));
}
On aurait aussi pu écrire v.iter().copied().fold(f64::MIN, f64::max), mais
cette version renverrait f64::MIN plutĂŽt que None pour une slice
vide â moins explicite. Retourner un Option force l'appelant Ă gĂ©rer le cas
« aucune donnée », ce qu'un -inf silencieux en C laisserait facilement passer
inaperçu.
Exercice 4 â Addition de deux vecteurs Ă©lĂ©ment par Ă©lĂ©ment
Ăcris une fonction addition(a: &[f64], b: &[f64]) -> Vec<f64>
qui renvoie un nouveau Vec contenant la somme terme Ă terme de deux slices de
mĂȘme longueur (on suppose ici qu'elles ont la mĂȘme longueur). Utilise
Vec::with_capacity pour réserver la place à l'avance plutÎt que de laisser le
Vec réallouer au fil des push.
Voir la solution
fn addition(a: &[f64], b: &[f64]) -> Vec<f64> {
debug_assert_eq!(a.len(), b.len(), "les deux vecteurs doivent avoir la mĂȘme longueur");
let mut resultat = Vec::with_capacity(a.len());
for (x, y) in a.iter().zip(b.iter()) {
resultat.push(x + y);
}
resultat
}
fn main() {
let vitesse = vec![0.4, -0.1, 0.05];
let correction = vec![0.02, 0.0, -0.01];
println!("somme = {:?}", addition(&vitesse, &correction));
}
Vec::with_capacity(a.len()) alloue une seule fois la mémoire nécessaire pour
tous les résultats : les appels suivants à push ne déclenchent aucune
réallocation, puisque la capacité réservée est suffisante dÚs le départ. On aurait aussi
pu Ă©crire a.iter().zip(b.iter()).map(|(x, y)| x + y).collect() â plus concis,
et collect rĂ©serve lui-mĂȘme la bonne capacitĂ© en amont.
Récapitulatif
[T; N]: taille fixe connue Ă la compilation, sur la pile, aucune allocation â le choix par dĂ©faut en temps rĂ©el quand la dimension est constante.Vec<T>: tableau dynamique sur le tas ;push/poppeuvent dĂ©clencher une rĂ©allocation coĂ»teuse â rĂ©server la capacitĂ© Ă l'avance avecVec::with_capacityhors du chemin chaud.&[T]/&mut [T]: vues empruntĂ©es, sans copie, sur un tableau ou unVecâ le type Ă privilĂ©gier dans les signatures de fonctions gĂ©nĂ©riques.v[i]panique siiest hors bornes (fail-fast et explicite) ;v.get(i)renvoie uneOptionpour gĂ©rer le cas sans jamais planter â contrairement au C, jamais de dĂ©passement silencieux..iter(),.iter_mut(),.enumerate(),.map(),.sum(),.zip(): la boĂźte Ă outils de base pour parcourir et combiner des collections, notamment pour les opĂ©rations vectorielles (norme, produit scalaire, addition).- En boucle de contrĂŽle temps rĂ©el : zĂ©ro allocation â tableaux fixes ou
Vecpré-alloués et réutilisés, jamais recréés à chaque itération.