Numeri casuali in Rust con rand
I giochi lanciano dadi. I quiz mescolano le domande. I test hanno bisogno di dati d’esempio. Per tutto questo servono numeri casuali, e la libreria standard di Rust non ha un generatore di numeri casuali pronto all’uso. Al suo posto quasi tutti usano un crate: rand (in inglese).
Questa pagina mostra l’API moderna di rand 0.10. È importante, perché rand ha cambiato alcuni nomi nelle versioni 0.9 e 0.10. Se un tutorial più vecchio mostra thread_rng() o gen_range(), è stato scritto per una versione precedente. Vedremo i nomi nuovi e gli errori che ottieni con quelli vecchi.
Aggiungere rand al progetto
Sezione intitolata “Aggiungere rand al progetto”Nella cartella del progetto, esegui:
cargo add randQuesto aggiunge al tuo Cargo.toml una riga come questa:
[dependencies]rand = "0.10"Se crate e Cargo.toml sono una novità per te, leggi prima quella pagina. I pulsanti «Prova» di questa pagina aprono il Rust Playground, che include già rand.
Valori casuali al volo
Sezione intitolata “Valori casuali al volo”Per un singolo valore casuale, usa le funzioni libere rand::random_range e rand::random:
fn main() { let die: u32 = rand::random_range(1..=6); let coin: bool = rand::random(); let chance: f64 = rand::random();
println!("il dado è tra 1 e 6: {}", (1..=6).contains(&die)); println!("chance è in 0.0..1.0: {}", (0.0..1.0).contains(&chance)); println!("la moneta è testa: {coin}");}Qui manca apposta il blocco di output: l’ultima riga è true o false a caso. Premi «Prova» qualche volta e guardala cambiare.
random_range(1..=6)dà un numero da 1 a 6, compreso il 6. Con1..6otterresti solo da 1 a 5. Gli intervalli funzionano come in un ciclo for.random()sceglie un valore del tipo che chiedi. Per unboolè un lancio di moneta equo. Per unf64è un numero da0.0fino a1.0(escluso).- L’annotazione di tipo (
: u32,: bool) dice arandcosa generare. Senza, Rust potrebbe non sapere quale tipo vuoi.
Un generatore e il trait RngExt
Sezione intitolata “Un generatore e il trait RngExt”Quando ti servono tanti valori casuali, prendi una volta sola un generatore con rand::rng() e chiama i metodi su di esso. È un po’ più veloce, ed è lo stile che vedrai nella maggior parte del codice:
use rand::RngExt;
fn main() { let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect(); println!("lanciati {} dadi", rolls.len()); println!("tutti tra 1 e 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random(); println!("x è in 0.0..1.0: {}", (0.0..1.0).contains(&x));}Output:
lanciati 1000 daditutti tra 1 e 6: truex è in 0.0..1.0: trueStampiamo dei controlli invece dei numeri stessi, così l’output è sempre uguale anche se i valori non lo sono.
Altri due metodi che userai spesso:
let lucky = rng.random_bool(0.1); // true circa il 10% delle voltelet rare = rng.random_ratio(1, 100); // true circa 1 volta su 100Perché la riga use rand::RngExt;? Metodi come random_range e random_bool vengono da un trait, e puoi chiamare i metodi di un trait solo quando il trait è nello scope. In rand 0.10 ci sono due trait:
Rngè il trait di base, «so produrre bit casuali». Ogni generatore lo implementa.RngExtaggiunge i metodi comodi:random,random_range,random_boole altri. Funziona in automatico su ogniRng.
Quindi: importa RngExt per chiamare i metodi, e usa Rng quando scrivi una funzione che accetta «qualsiasi generatore». Li vedrai entrambi nel lanciatore di dadi più sotto.
Risultati riproducibili con un seed
Sezione intitolata “Risultati riproducibili con un seed”rand::rng() dà numeri diversi a ogni esecuzione. È quello che vuoi in un gioco vero. Ma per i test, il debug o il livello «sfida del giorno» di un gioco, vuoi gli stessi numeri «casuali» ogni volta.
Per questo c’è il seed (seme). Un generatore con seed produce sempre la stessa sequenza a parità di seed:
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!("uguali? {}", first == second);}Output:
[53, 82, 74, 51][53, 82, 74, 51]uguali? trueseed_from_u64 viene dal trait SeedableRng, quindi va importato anche quello. Cambia 2024 con un altro numero e ottieni una sequenza diversa, ma sempre la stessa a ogni esecuzione.
Un avvertimento onesto: l’algoritmo di StdRng può cambiare in una futura release di rand. I numeri qui sopra vengono da rand 0.10. Se i tuoi test devono dare esattamente gli stessi numeri per sempre, il libro di rand consiglia un generatore con un nome preciso come ChaCha8Rng (vedi «Per approfondire» più sotto).
Mescolare e scegliere da una slice
Sezione intitolata “Mescolare e scegliere da una slice”Per lavorare con le liste, importa altri due trait da rand::seq:
SliceRandomfornisceshuffle, che mescola una slice sul posto.IndexedRandomforniscechoose(un elemento a caso) esample(più elementi diversi).
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!["A", "K", "Q", "J", "10"]; cards.shuffle(&mut rng); println!("mescolate: {:?}", cards);
let colors = ["rosso", "verde", "blu"]; println!("scelto: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect(); println!("squadra: {:?}", team);
let empty: [i32; 0] = []; println!("da vuota: {:?}", empty.choose(&mut rng));}Output:
mescolate: ["K", "J", "10", "Q", "A"]scelto: Some("rosso")squadra: ["K", "J"]da vuota: Nonechoose restituisce un Option, perché in una slice vuota non c’è niente da scegliere. Questi metodi funzionano allo stesso modo su array, vettori e slice. Sostituisci l’rng con seed con rand::rng() e otterrai un mescolamento nuovo a ogni esecuzione.
Esempio: un lanciatore di dadi
Sezione intitolata “Esempio: un lanciatore di dadi”Scriviamo una funzione che lancia dei dadi. Il trucco è il tipo del parametro: &mut impl Rng significa «qualsiasi generatore casuale». Nel gioco vero passi rand::rng(). In un test passi uno StdRng con seed e sai esattamente cosa ne esce:
use rand::rngs::StdRng;use rand::{Rng, RngExt, SeedableRng};
/// Lancia `count` dadi con `sides` facce ciascuno.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); }}Output:
2d6: [5, 6] = 111d20: [16] = 163d4: [3, 1, 2] = 62d6 è l’abbreviazione dei giochi da tavolo per «due dadi a sei facce». Passare il generatore come parametro, invece di chiamare rand::rng() dentro roll, è una buona abitudine: rende la funzione facile da testare.
Esempio: un generatore di password casuali
Sezione intitolata “Esempio: un generatore di password casuali”Qui scegliamo 16 caratteri da un insieme consentito. L’insieme esclude i caratteri che si somigliano, come 0/O e 1/l, così la password è più facile da digitare:
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!("lunghezza: {}", pw.len()); println!("solo caratteri consentiti: {}", pw.bytes().all(|b| CHARSET.contains(&b))); // println!("{pw}"); // togli il commento per vederla: cambia a ogni esecuzione}Output:
lunghezza: 16solo caratteri consentiti: trueb"..." è una stringa di byte: una &[u8] di caratteri ASCII. choose sceglie un byte, e as char lo trasforma in un carattere. Nota che qui abbiamo usato rand::rng(), mai un generatore con seed. Una password ottenuta da un seed fisso è sempre la stessa password, cioè non è affatto una password.
Ritorno al gioco «indovina il numero»
Sezione intitolata “Ritorno al gioco «indovina il numero»”Il progetto «indovina il numero» sceglie il suo numero segreto con un trucco basato sull’orologio, così può restare solo con la libreria standard. Con rand, l’intera funzione secret_number diventa una riga:
let secret: u32 = rand::random_range(1..=100);Tutto il resto del gioco resta uguale. Ecco una versione che puoi eseguire nel Playground. Il segreto viene da un generatore con seed, e il computer tira a indovinare scegliendo sempre il centro dell’intervallo rimasto:
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};use std::cmp::Ordering;
fn main() { // Nel gioco vero: 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} è troppo piccolo"); low = guess + 1; } Ordering::Greater => { println!("{guess} è troppo grande"); high = guess - 1; } Ordering::Equal => { println!("{guess} è giusto! Trovato in {attempts} tentativi."); break; } } }}Output:
50 è troppo piccolo75 è troppo piccolo88 è troppo piccolo94 è troppo piccolo97 è troppo piccolo99 è giusto! Trovato in 6 tentativi.Curiosità: questa strategia del «dimezza l’intervallo» non ha mai bisogno di più di 7 tentativi per i numeri da 1 a 100.
rand è sicuro per password e chiavi?
Sezione intitolata “rand è sicuro per password e chiavi?”Risposta breve: rand::rng() è progettato per essere difficile da prevedere, ma per i segreti seri usa strumenti fatti apposta.
rand::rng()usa un algoritmo crittograficamente sicuro (attualmente ChaCha12). Riceve il seed dalla sorgente casuale del sistema operativo, tramite il crategetrandom(in inglese), e si rigenera regolarmente. La documentazione di rand lo definisce un «generatore veloce e ragionevolmente sicuro», e dice di non poter promettere che vada bene per ogni esigenza di sicurezza.- Per chiavi di crittografia, token di API e altri segreti importanti, prendi i byte direttamente dal sistema operativo con
getrandom(orand::rngs::SysRng), o meglio ancora lascia che sia una libreria crittografica dedicata a generare la chiave. - Mai usare un generatore con seed (
seed_from_u64) o il veloceSmallRngper qualcosa di segreto. Chiunque conosca o indovini il seed può produrre gli stessi valori «casuali».
Per giochi, simulazioni, mescolamenti e dati di test, rand::rng() è esattamente quello che serve.
Errori comuni
Sezione intitolata “Errori comuni”Importare Rng invece di RngExt. In rand 0.9 i metodi stavano su Rng, quindi il codice più vecchio fa use rand::Rng;. Con rand 0.10 questo dà:
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; |Il compilatore dice che ThreadRng non ha nessun metodo random_range nello scope attuale, e ti suggerisce già la soluzione: use rand::RngExt;.
Copiare codice scritto per rand 0.8. Gli esempi molto vecchi usano thread_rng() e 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`Il compilatore non trova la funzione thread_rng nel crate rand. I nomi nuovi sono rand::rng() e random_range(). Allo stesso modo, gen() è diventato random() e gen_bool() è diventato random_bool().
Intervalli sbagliati di uno. random_range(1..6) non restituisce mai 6. Per un dado, scrivi 1..=6.
Riepilogo
Sezione intitolata “Riepilogo”- Aggiungi il crate con
cargo add rand. La libreria standard di Rust non ha un suo generatore di numeri casuali. - Per valori al volo usa
rand::random_range(1..=6)erand::random::<T>(). - Per tanti valori usa
let mut rng = rand::rng();conuse rand::RngExt;per metodi comerandom_rangeerandom_bool. SliceRandom::shufflemescola una slice;IndexedRandom::choosesceglie un elemento e restituisce unOption.StdRng::seed_from_u64(n)dà la stessa sequenza a ogni esecuzione: perfetto per i test e per livelli di gioco ripetibili, mai per i segreti.- Per chiavi e token, usa la sorgente casuale del sistema operativo (
getrandom) o un crate crittografico dedicato.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale chiamata può restituire 6?
2. Con rand 0.10, rng.random_range(..) fallisce con "no method named random_range". Cosa manca?
3. Perché useresti StdRng::seed_from_u64(42)?
4. Cosa restituisce choose quando lo chiami su una slice vuota?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- The Rust Rand Book (in inglese): la guida ufficiale a
rand, dall’avvio rapido alla scelta di un generatore. - Documentazione dell’API di
rand— docs.rs (in inglese): ogni funzione, trait e generatore, con esempi. - Updating to 0.10 — The Rust Rand Book (in inglese): cosa è cambiato nella 0.10, compresa la ridenominazione
Rng/RngExt. Utile quando incontri codice più vecchio. - Our RNGs — The Rust Rand Book (in inglese): confronta i generatori, indicando anche quali sono adatti alla sicurezza.
- Reproducibility — The Rust Rand Book (in inglese): come mantenere stabili i risultati con seed tra versioni e piattaforme.