Les nombres aléatoires en Rust avec rand
Les jeux lancent des dés. Les quiz mélangent leurs questions. Les tests ont besoin de données d’exemple. Tout cela demande des nombres aléatoires, et la bibliothèque standard de Rust ne propose pas de générateur de nombres aléatoires prêt à l’emploi. À la place, presque tout le monde utilise une seule crate : rand (en anglais).
Cette page présente l’API moderne de rand 0.10. C’est important, car rand a changé certains noms dans les versions 0.9 et 0.10. Si un ancien tutoriel montre thread_rng() ou gen_range(), il a été écrit pour une version plus ancienne. Nous verrons les nouveaux noms, ainsi que les erreurs que provoquent les anciens.
Ajouter rand à votre projet
Section intitulée « Ajouter rand à votre projet »Dans le dossier de votre projet, lancez :
cargo add randCela ajoute une ligne de ce genre à votre Cargo.toml :
[dependencies]rand = "0.10"Si les crates et Cargo.toml sont nouveaux pour vous, lisez d’abord cette page. Les boutons « Essayer » de cette page ouvrent le Playground Rust, qui inclut déjà rand.
Des valeurs aléatoires en un clin d’œil
Section intitulée « Des valeurs aléatoires en un clin d’œil »Pour obtenir une seule valeur aléatoire, utilisez les fonctions libres rand::random_range et rand::random :
fn main() { let die: u32 = rand::random_range(1..=6); let coin: bool = rand::random(); let chance: f64 = rand::random();
println!("le dé est entre 1 et 6 : {}", (1..=6).contains(&die)); println!("chance est dans 0.0..1.0 : {}", (0.0..1.0).contains(&chance)); println!("la pièce tombe sur face : {coin}");}Il n’y a volontairement pas de bloc de sortie ici : la dernière ligne affiche true ou false au hasard. Cliquez plusieurs fois sur « Essayer » et regardez-la changer.
random_range(1..=6)donne un nombre de 1 à 6, 6 compris. Avec1..6, vous n’obtiendriez que 1 à 5. Les intervalles fonctionnent comme dans une boucle for.random()choisit une valeur du type que vous demandez. Pour unbool, c’est un pile ou face équitable. Pour unf64, c’est un nombre entre0.0(compris) et1.0(non compris).- L’annotation de type (
: u32,: bool) indique àrandce qu’il doit fabriquer. Sans elle, Rust risque de ne pas savoir quel type vous voulez.
Un générateur et le trait RngExt
Section intitulée « Un générateur et le trait RngExt »Quand vous avez besoin de beaucoup de valeurs aléatoires, récupérez une fois pour toutes un générateur avec rand::rng(), puis appelez ses méthodes. C’est un peu plus rapide, et c’est le style que vous verrez dans la plupart des programmes :
use rand::RngExt;
fn main() { let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect(); println!("{} dés lancés", rolls.len()); println!("tous entre 1 et 6 : {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random(); println!("x est dans 0.0..1.0 : {}", (0.0..1.0).contains(&x));}Sortie :
1000 dés lancéstous entre 1 et 6 : truex est dans 0.0..1.0 : trueNous affichons des vérifications plutôt que les nombres eux-mêmes : ainsi, la sortie est la même à chaque fois, même si les valeurs changent.
Deux autres méthodes que vous utiliserez souvent :
let lucky = rng.random_bool(0.1); // true environ 10 % du tempslet rare = rng.random_ratio(1, 100); // true environ 1 fois sur 100Pourquoi la ligne use rand::RngExt; ? Les méthodes comme random_range et random_bool viennent d’un trait, et on ne peut appeler les méthodes d’un trait que si ce trait est dans la portée. Dans rand 0.10, il y a deux traits :
Rngest le trait de base, « je sais produire des bits aléatoires ». Chaque générateur l’implémente.RngExtajoute les méthodes pratiques :random,random_range,random_boolet d’autres. Il fonctionne automatiquement sur toutRng.
En résumé : importez RngExt pour appeler les méthodes, et utilisez Rng quand vous écrivez une fonction qui accepte « n’importe quel générateur ». Vous verrez les deux dans le lanceur de dés plus bas.
Des résultats reproductibles avec une graine
Section intitulée « Des résultats reproductibles avec une graine »rand::rng() donne des nombres différents à chaque exécution. C’est ce qu’on veut dans un vrai jeu. Mais pour les tests, le débogage ou le niveau « défi du jour » d’un jeu, on veut les mêmes nombres « aléatoires » à chaque fois.
C’est le rôle d’une graine (seed). Un générateur initialisé avec une graine produit toujours la même suite pour la même graine :
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};
fn main() { let mut a = StdRng::seed_from_u64(2024); let mut b = StdRng::seed_from_u64(2024);
let first: Vec<u8> = (0..4).map(|_| a.random_range(0..100)).collect(); let second: Vec<u8> = (0..4).map(|_| b.random_range(0..100)).collect();
println!("{:?}", first); println!("{:?}", second); println!("identiques ? {}", first == second);}Sortie :
[53, 82, 74, 51][53, 82, 74, 51]identiques ? trueseed_from_u64 vient du trait SeedableRng, qu’il faut donc importer aussi. Remplacez 2024 par un autre nombre et vous obtiendrez une autre suite, mais toujours la même d’une exécution à l’autre.
Un avertissement honnête : l’algorithme de StdRng a le droit de changer dans une future version de rand. Les nombres ci-dessus viennent de rand 0.10. Si vos tests doivent donner exactement les mêmes nombres pour toujours, le rand book recommande un générateur nommé comme ChaCha8Rng (voir « Pour aller plus loin » en bas de page).
Mélanger une slice et y piocher
Section intitulée « Mélanger une slice et y piocher »Pour travailler avec des listes, importez deux traits de plus depuis rand::seq :
SliceRandomfournitshuffle, qui mélange une slice sur place.IndexedRandomfournitchoose(un élément au hasard) etsample(plusieurs éléments différents).
use rand::rngs::StdRng;use rand::seq::{IndexedRandom, SliceRandom};use rand::SeedableRng;
fn main() { let mut rng = StdRng::seed_from_u64(7);
let mut cards = vec!["As", "Roi", "Dame", "Valet", "10"]; cards.shuffle(&mut rng); println!("mélangées : {:?}", cards);
let colors = ["rouge", "vert", "bleu"]; println!("choisie : {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect(); println!("équipe : {:?}", team);
let empty: [i32; 0] = []; println!("dans un tableau vide : {:?}", empty.choose(&mut rng));}Sortie :
mélangées : ["Roi", "Valet", "10", "Dame", "As"]choisie : Some("rouge")équipe : ["Roi", "Valet"]dans un tableau vide : Nonechoose renvoie une Option, car une slice vide n’a rien à proposer. Ces méthodes fonctionnent aussi bien sur les tableaux, les vecteurs et les slices. Remplacez le rng à graine par rand::rng() et vous obtiendrez un nouveau mélange à chaque exécution.
Exemple : un lanceur de dés
Section intitulée « Exemple : un lanceur de dés »Écrivons une fonction qui lance des dés. L’astuce est dans le type du paramètre : &mut impl Rng signifie « n’importe quel générateur aléatoire ». Dans le vrai jeu, vous passez rand::rng(). Dans un test, vous passez un StdRng avec une graine, et vous savez exactement ce qui va sortir :
use rand::rngs::StdRng;use rand::{Rng, RngExt, SeedableRng};
/// Lance `count` dés de `sides` faces chacun.fn roll(rng: &mut impl Rng, count: u32, sides: u32) -> Vec<u32> { (0..count).map(|_| rng.random_range(1..=sides)).collect()}
fn main() { let mut rng = StdRng::seed_from_u64(1);
for (count, sides) in [(2, 6), (1, 20), (3, 4)] { let rolls = roll(&mut rng, count, sides); let total: u32 = rolls.iter().sum(); println!("{count}d{sides} : {:?} = {total}", rolls); }}Sortie :
2d6 : [5, 6] = 111d20 : [16] = 163d4 : [3, 1, 2] = 62d6 est la notation des jeux de société et de rôle pour « deux dés à six faces ». Passer le générateur en paramètre, plutôt que d’appeler rand::rng() à l’intérieur de roll, est une bonne habitude : la fonction reste facile à tester.
Exemple : un générateur de mots de passe
Section intitulée « Exemple : un générateur de mots de passe »Ici, nous choisissons 16 caractères dans un ensemble autorisé. Cet ensemble exclut les caractères qui se ressemblent, comme 0/O et 1/l, pour que le mot de passe soit plus facile à taper :
use rand::seq::IndexedRandom;use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*";
fn password(rng: &mut impl Rng, length: usize) -> String { (0..length) .map(|_| *CHARSET.choose(rng).unwrap() as char) .collect()}
fn main() { let mut rng = rand::rng(); let pw = password(&mut rng, 16);
println!("longueur : {}", pw.len()); println!("uniquement des caractères autorisés : {}", pw.bytes().all(|b| CHARSET.contains(&b))); // println!("{pw}"); // décommentez pour le voir : différent à chaque exécution}Sortie :
longueur : 16uniquement des caractères autorisés : trueb"..." est une chaîne d’octets : un &[u8] de caractères ASCII. choose choisit un octet, et as char le transforme en caractère. Remarquez que nous avons utilisé rand::rng() ici, et jamais un générateur à graine. Un mot de passe issu d’une graine fixe est le même à chaque fois, ce qui revient à ne pas avoir de mot de passe du tout.
Retour au jeu de devinettes
Section intitulée « Retour au jeu de devinettes »Le projet de jeu de devinettes choisit son nombre secret avec une astuce basée sur l’horloge, pour n’utiliser que la bibliothèque standard. Avec rand, toute la fonction secret_number tient en une ligne :
let secret: u32 = rand::random_range(1..=100);Tout le reste du jeu ne change pas. Voici une version que vous pouvez lancer dans le Playground. Le secret vient d’un générateur à graine, et l’ordinateur devine en choisissant toujours le milieu de l’intervalle restant :
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};use std::cmp::Ordering;
fn main() { // Dans le vrai jeu : let secret: u32 = rand::random_range(1..=100); let mut rng = StdRng::seed_from_u64(99); let secret: u32 = rng.random_range(1..=100);
let (mut low, mut high) = (1, 100); let mut attempts = 0; loop { let guess = (low + high) / 2; attempts += 1; match guess.cmp(&secret) { Ordering::Less => { println!("{guess} est trop petit"); low = guess + 1; } Ordering::Greater => { println!("{guess} est trop grand"); high = guess - 1; } Ordering::Equal => { println!("{guess} est le bon ! Trouvé en {attempts} essais."); break; } } }}Sortie :
50 est trop petit75 est trop petit88 est trop petit94 est trop petit97 est trop petit99 est le bon ! Trouvé en 6 essais.Le saviez-vous ? Cette stratégie qui « coupe l’intervalle en deux » n’a jamais besoin de plus de 7 essais pour un nombre entre 1 et 100.
rand est-il sûr pour les mots de passe et les clés ?
Section intitulée « rand est-il sûr pour les mots de passe et les clés ? »Réponse courte : rand::rng() est conçu pour être difficile à prédire, mais pour les secrets sérieux, utilisez des outils faits pour cela.
rand::rng()utilise un algorithme cryptographiquement sûr (actuellement ChaCha12). Il reçoit sa graine de la source aléatoire de votre système d’exploitation, via la crategetrandom(en anglais), et se réinitialise régulièrement. La documentation de rand le décrit comme un « générateur rapide et raisonnablement sûr », et précise qu’elle ne peut pas promettre qu’il convienne à tous les besoins de sécurité.- Pour les clés de chiffrement, les jetons d’API et les autres secrets à fort enjeu, prenez les octets directement auprès du système d’exploitation avec
getrandom(ourand::rngs::SysRng), ou mieux, laissez une bibliothèque de cryptographie spécialisée générer la clé pour vous. - N’utilisez jamais un générateur à graine (
seed_from_u64) ni le rapideSmallRngpour quoi que ce soit de secret. Quiconque connaît ou devine la graine peut reproduire les mêmes valeurs « aléatoires ».
Pour les jeux, les simulations, les mélanges et les données de test, rand::rng() est exactement ce qu’il faut.
Erreurs courantes
Section intitulée « Erreurs courantes »Importer Rng au lieu de RngExt. Dans rand 0.9, les méthodes se trouvaient sur Rng, donc l’ancien code fait use rand::Rng;. Avec rand 0.10, cela donne :
use rand::Rng;
fn main() { let mut rng = rand::rng(); let n = rng.random_range(1..=6); println!("{n}");}error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope --> src/main.rs:5:17 |5 | let n = rng.random_range(1..=6); | ^^^^^^^^^^^^ | = help: items from traits can only be used if the trait is in scopehelp: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it |1 + use rand::RngExt; |Le compilateur (en anglais) explique qu’aucune méthode random_range n’est disponible, car le trait qui la fournit n’est pas dans la portée. Il vous donne même la solution : use rand::RngExt;.
Copier du code écrit pour rand 0.8. Les très vieux exemples utilisent thread_rng() et gen_range() :
fn main() { let n = rand::thread_rng().gen_range(1..=6); println!("{n}");}error[E0425]: cannot find function `thread_rng` in crate `rand` --> src/main.rs:2:19 |2 | let n = rand::thread_rng().gen_range(1..=6); | ^^^^^^^^^^ not found in `rand`Autrement dit : « la fonction thread_rng n’existe pas dans la crate rand ». Les nouveaux noms sont rand::rng() et random_range(). De même, gen() est devenu random() et gen_bool() est devenu random_bool().
Les intervalles décalés d’un cran. random_range(1..6) ne renvoie jamais 6. Pour un dé, écrivez 1..=6.
Récapitulatif
Section intitulée « Récapitulatif »- Ajoutez la crate avec
cargo add rand. La bibliothèque standard de Rust n’a pas de générateur de nombres aléatoires à elle. - Pour des valeurs ponctuelles, utilisez
rand::random_range(1..=6)etrand::random::<T>(). - Pour beaucoup de valeurs, utilisez
let mut rng = rand::rng();avecuse rand::RngExt;pour accéder aux méthodes commerandom_rangeetrandom_bool. SliceRandom::shufflemélange une slice ;IndexedRandom::choosechoisit un élément et renvoie uneOption.StdRng::seed_from_u64(n)donne la même suite à chaque exécution : parfait pour les tests et les niveaux de jeu reproductibles, jamais pour des secrets.- Pour les clés et les jetons, utilisez la source aléatoire du système (
getrandom) ou une crate de cryptographie spécialisée.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel appel peut renvoyer 6 ?
2. Avec rand 0.10, rng.random_range(..) échoue avec « no method named random_range ». Que manque-t-il ?
3. Pourquoi utiliser StdRng::seed_from_u64(42) ?
4. Que renvoie choose quand on l'appelle sur une slice vide ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- The Rust Rand Book (en anglais) : le guide officiel de
rand, du démarrage rapide au choix d’un générateur. - Documentation de l’API
rand— docs.rs (en anglais) : chaque fonction, trait et générateur, avec des exemples. - Updating to 0.10 — The Rust Rand Book (en anglais) : ce qui a changé dans la 0.10, dont le renommage
Rng/RngExt. Utile quand vous tombez sur du code plus ancien. - Our RNGs — The Rust Rand Book (en anglais) : une comparaison des générateurs, y compris ceux qui conviennent à la sécurité.
- Reproducibility — The Rust Rand Book (en anglais) : comment garder des résultats à graine stables d’une version et d’une plateforme à l’autre.