Vecteur de zéros en Rust : pas aussi simple qu'il n'y paraît
\n\nOn pourrait penser que quoi de plus simple que de créer un vecteur rempli de zéros ? En Python, cela se fait en une ligne : [0] * n. En JavaScript — Array(n).fill(0). Mais quand vous passez à Rust, même une tâche aussi triviale ouvre tout un monde de programmation système, de gestion de la mémoire et de coûts d'abstraction nuls.
La question "Creating a vector of zeros for a specific size" sur Stack Overflow a rassemblé des milliers de vues. Et ce n'est pas un hasard : derrière une simple requête se cache une compréhension profonde de la façon dont Rust gère la mémoire, l'initialisation et la sécurité. Aujourd'hui, nous allons regarder sous le capot de Rust et analyser toutes les façons de créer un vecteur de zéros, et surtout — comprendre ce qui se passe réellement dans la mémoire vive.
\n\nPourquoi est-ce plus complexe qu'en Python ?
\n\nEn Python ou JavaScript, vous créez simplement un tableau, et l'interpréteur fait tout le travail à votre place. Rust, quant à lui, est un langage de niveau système. Il ne permet pas de laisser la mémoire non initialisée (sauf si vous utilisez unsafe). Chaque octet doit être soit explicitement rempli, soit marqué comme "comportement indéfini".
Imaginez : vous commandez 100 boîtes vides. En Python, on vous livre simplement 100 boîtes vides. Avec Rust, le livreur demande : "Que dois-je mettre à l'intérieur de chacune ?" — et si vous ne le dites pas, il ne vient tout simplement pas. Cela garantit que vous ne lirez jamais accidentellement des "déchets" mémoire laissés par un programme précédent.
\n\nMéthode n°1 : La classique vec![0; size]
\n\nLa méthode la plus évidente et recommandée est d'utiliser la macro vec! :
let zeros: Vec<i32> = vec![0; 10];\nprintln!("{:?}", zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n\nCourt, élégant, clair. Mais que se passe-t-il sous le capot ? Analysons étape par étape.
\n\nSous le capot de vec![0; 10]
\n\nQuand vous écrivez vec![0; 10], Rust fait ce qui suit :
- \n
- Alloue la mémoire sur le tas (heap) pour 10 éléments de type
i32. Chaquei32occupe 4 octets, soit 40 octets au total. \n - Vérifie que le type implémente le trait
Clone. Les nombresi32l'implémentent, donc le compilateur peut copier la valeur0pour chaque élément. \n - Lance une boucle de copie à l'intérieur de
alloc::vec::from_elem. Le compilateur optimise cette boucle enmemset(si le type est un scalaire simple). \n - Retourne un
Vecavec une longueur de 10 et une capacité de 10. \n
Point clé : la mémoire ne reste pas non initialisée. Rust garantit que chaque octet sera rempli de zéros avant que vous n'ayez accès au vecteur.
\n\nMéthode n°2 : vec![Default::default(); size]
\n\nSi votre type n'est pas un nombre mais implémente le trait Default, vous pouvez utiliser une approche générique :
#[derive(Default)]\nstruct Point {\n x: f64,\n y: f64,\n}\n\nlet points: Vec<Point> = vec![Default::default(); 5];\nprintln!("{:?}", points[0]); // Point { x: 0.0, y: 0.0 }\n\nPour les types primitifs, Default::default() donne 0, 0.0, false ou '\\0'. Pour les structures personnalisées — ce que vous avez défini dans #[derive(Default)].
Et qu'en est-il des performances ?
\n\nÉcrivons un simple benchmark (dans du code réel, utilisez criterion) :
use std::time::Instant;\n\nfn main() {\n let n = 1_000_000;\n\n let start = Instant::now();\n let v1 = vec![0u8; n];\n let dur1 = start.elapsed();\n\n let start = Instant::now();\n let v2 = vec![0u64; n];\n let dur2 = start.elapsed();\n\n println!("u8: {:?}", dur1);\n println!("u64: {:?}", dur2);\n}\n\nEn