Ir al contenido

Números aleatorios en Rust

Los juegos tiran dados. Los cuestionarios barajan sus preguntas. Los tests necesitan datos de prueba. Todo eso necesita números aleatorios, y la biblioteca estándar de Rust no trae un generador de números aleatorios listo para usar. En su lugar, casi todo el mundo usa un crate: rand (en inglés).

Esta página muestra la API moderna de rand 0.10. Eso importa, porque rand cambió algunos nombres en las versiones 0.9 y 0.10. Si un tutorial antiguo usa thread_rng() o gen_range(), se escribió para una versión anterior. Veremos los nombres nuevos y los errores que te dan los antiguos.

En la carpeta de tu proyecto, ejecuta:

Terminal window
cargo add rand

Eso añade una línea como esta a tu Cargo.toml:

[dependencies]
rand = "0.10"

Si los crates y el Cargo.toml son nuevos para ti, lee esa página primero. Los botones para probar el código de esta página abren el Playground de Rust, que ya incluye rand.

Para un valor aleatorio suelto, usa las funciones libres rand::random_range y rand::random:

fn main() {
let die: u32 = rand::random_range(1..=6);
let coin: bool = rand::random();
let chance: f64 = rand::random();
println!("el dado está entre 1 y 6: {}", (1..=6).contains(&die));
println!("chance está en 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("la moneda salió cara: {coin}");
}
▶ Pruébalo tú mismo

Aquí no hay bloque de salida a propósito: la última línea es true o false al azar. Pulsa el botón varias veces y verás cómo cambia.

  • random_range(1..=6) da un número del 1 al 6, incluido el 6. Con 1..6 solo obtendrías del 1 al 5. Los rangos funcionan igual que en un bucle for.
  • random() elige un valor del tipo que le pidas. Para un bool, es lanzar una moneda al aire. Para un f64, es un número desde 0.0 hasta 1.0 (sin incluir el 1.0).
  • La anotación de tipo (: u32, : bool) le dice a rand qué tiene que generar. Sin ella, puede que Rust no sepa qué tipo quieres.

Cuando necesitas muchos valores aleatorios, obtén un generador una sola vez con rand::rng() y llama a sus métodos. Es un poco más rápido, y es el estilo que verás en casi todo el código:

use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect();
println!("tiramos {} dados", rolls.len());
println!("todos entre 1 y 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x está en 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Salida:

tiramos 1000 dados
todos entre 1 y 6: true
x está en 0.0..1.0: true
▶ Pruébalo tú mismo

Imprimimos comprobaciones en vez de los números, para que la salida sea la misma cada vez aunque los valores no lo sean.

Otros dos métodos que usarás a menudo:

let lucky = rng.random_bool(0.1); // true más o menos el 10% de las veces
let rare = rng.random_ratio(1, 100); // true más o menos 1 de cada 100 veces

¿Por qué la línea use rand::RngExt;? Métodos como random_range y random_bool vienen de un trait, y solo puedes llamar a los métodos de un trait cuando el trait está en el ámbito (scope). En rand 0.10 hay dos traits:

  • Rng es el trait básico de “sé producir bits aleatorios”. Todos los generadores lo implementan.
  • RngExt añade los métodos cómodos: random, random_range, random_bool y más. Funciona automáticamente con cualquier Rng.

Así que: importa RngExt para llamar a los métodos, y usa Rng cuando escribas una función que acepte “cualquier generador”. Verás los dos en el lanzador de dados de más abajo.

rand::rng() da números distintos en cada ejecución. Eso es lo que quieres en un juego de verdad. Pero para los tests, para depurar o para el nivel de “reto diario” de un juego, quieres los mismos números “aleatorios” cada vez.

Para eso sirve una semilla (seed). Un generador con semilla siempre produce la misma secuencia para la misma semilla:

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!("¿iguales? {}", first == second);
}

Salida:

[53, 82, 74, 51]
[53, 82, 74, 51]
¿iguales? true
▶ Pruébalo tú mismo

seed_from_u64 viene del trait SeedableRng, así que también hay que importarlo. Cambia 2024 por otro número y obtendrás otra secuencia, pero de nuevo la misma en cada ejecución.

Una advertencia honesta: el algoritmo de StdRng puede cambiar en una versión futura de rand. Los números de arriba salen de rand 0.10. Si tus tests tienen que dar exactamente los mismos números para siempre, el libro de rand recomienda un generador con nombre propio, como ChaCha8Rng (consulta “Para leer más”, abajo).

Para trabajar con listas, importa dos traits más de rand::seq:

  • SliceRandom da shuffle, que mezcla un slice en el sitio.
  • IndexedRandom da choose (un elemento al azar) y sample (varios elementos distintos).
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!("barajadas: {:?}", cards);
let colors = ["rojo", "verde", "azul"];
println!("elegido: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("equipo: {:?}", team);
let empty: [i32; 0] = [];
println!("de un slice vacío: {:?}", empty.choose(&mut rng));
}

Salida:

barajadas: ["K", "J", "10", "Q", "A"]
elegido: Some("rojo")
equipo: ["K", "J"]
de un slice vacío: None
▶ Pruébalo tú mismo

choose devuelve un Option, porque en un slice vacío no hay nada que elegir. Estos métodos funcionan igual con arrays, vectores y slices. Cambia el rng con semilla por rand::rng() y obtendrás una mezcla nueva en cada ejecución.

Vamos a escribir una función que tira dados. El truco está en el tipo del parámetro: &mut impl Rng significa “cualquier generador aleatorio”. En el juego de verdad le pasas rand::rng(). En un test le pasas un StdRng con semilla y sabes exactamente qué va a salir:

use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
/// Tira `count` dados de `sides` caras cada uno.
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);
}
}

Salida:

2d6: [5, 6] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ Pruébalo tú mismo

2d6 es la forma abreviada de los juegos de mesa y de rol para decir “dos dados de seis caras”. Pasar el generador como parámetro, en vez de llamar a rand::rng() dentro de roll, es una buena costumbre: hace que la función sea fácil de probar.

Ejemplo: un generador de contraseñas aleatorias

Sección titulada «Ejemplo: un generador de contraseñas aleatorias»

Aquí elegimos 16 caracteres de un conjunto permitido. El conjunto deja fuera caracteres que se parecen entre sí, como 0/O y 1/l, para que la contraseña sea más fácil de escribir:

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!("longitud: {}", pw.len());
println!("solo caracteres permitidos: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // quita el comentario para verla: cambia en cada ejecución
}

Salida:

longitud: 16
solo caracteres permitidos: true
▶ Pruébalo tú mismo

b"..." es una cadena de bytes: un &[u8] de caracteres ASCII. choose elige un byte y as char lo convierte en un carácter. Fíjate en que aquí usamos rand::rng(), nunca un generador con semilla. Una contraseña que sale de una semilla fija es la misma contraseña cada vez, y eso no es ninguna contraseña.

El proyecto del juego de adivinanzas elige su número secreto con un truco basado en el reloj, para poder usar solo la biblioteca estándar. Con rand, toda la función secret_number se queda en una línea:

let secret: u32 = rand::random_range(1..=100);

Todo lo demás del juego sigue igual. Aquí tienes una versión que puedes ejecutar en el Playground. El secreto sale de un generador con semilla, y el ordenador adivina eligiendo siempre el centro del rango que queda:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// En el juego de verdad: 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} es demasiado pequeño"); low = guess + 1; }
Ordering::Greater => { println!("{guess} es demasiado grande"); high = guess - 1; }
Ordering::Equal => { println!("¡{guess} es correcto! Encontrado en {attempts} intentos."); break; }
}
}
}

Salida:

50 es demasiado pequeño
75 es demasiado pequeño
88 es demasiado pequeño
94 es demasiado pequeño
97 es demasiado pequeño
¡99 es correcto! Encontrado en 6 intentos.
▶ Pruébalo tú mismo

Dato curioso: esta estrategia de “partir el rango por la mitad” nunca necesita más de 7 intentos para un número del 1 al 100.

¿Es rand seguro para contraseñas y claves?

Sección titulada «¿Es rand seguro para contraseñas y claves?»

Respuesta corta: rand::rng() está diseñado para ser difícil de predecir, pero para secretos serios usa herramientas hechas para eso.

  • rand::rng() usa un algoritmo criptográficamente seguro (ahora mismo, ChaCha12). Recibe su semilla de la fuente aleatoria de tu sistema operativo, a través del crate getrandom (en inglés), y se vuelve a sembrar cada cierto tiempo. La documentación de rand lo llama un “generador rápido y razonablemente seguro”, y dice que no puede prometer que sirva para cualquier necesidad de seguridad.
  • Para claves de cifrado, tokens de API y otros secretos importantes, obtén los bytes directamente del sistema operativo con getrandom (o rand::rngs::SysRng) o, mejor todavía, deja que una biblioteca de criptografía especializada genere la clave por ti.
  • Nunca uses un generador con semilla (seed_from_u64) ni el rápido SmallRng para nada secreto. Cualquiera que conozca o adivine la semilla puede producir los mismos valores “aleatorios”.

Para juegos, simulaciones, barajar y datos de prueba, rand::rng() es justo lo que necesitas.

Importar Rng en vez de RngExt. En rand 0.9 los métodos estaban en Rng, así que el código antiguo hace use rand::Rng;. Con rand 0.10 eso da:

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 scope
help: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it
|
1 + use rand::RngExt;
|

El compilador dice que no encuentra el método random_range porque el trait que lo proporciona no está en el ámbito, y te da la solución: use rand::RngExt;.

Copiar código escrito para rand 0.8. Los ejemplos muy antiguos usan thread_rng() y 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`

El error dice que la función thread_rng no existe en el crate rand. Los nombres nuevos son rand::rng() y random_range(). Del mismo modo, gen() pasó a ser random() y gen_bool() pasó a ser random_bool().

Rangos que se quedan cortos por uno. random_range(1..6) nunca devuelve 6. Para un dado, escribe 1..=6.

  • Añade el crate con cargo add rand. La biblioteca estándar de Rust no tiene su propio generador de números aleatorios.
  • Para valores rápidos usa rand::random_range(1..=6) y rand::random::<T>().
  • Para muchos valores, usa let mut rng = rand::rng(); con use rand::RngExt; para métodos como random_range y random_bool.
  • SliceRandom::shuffle mezcla un slice; IndexedRandom::choose elige un elemento y devuelve un Option.
  • StdRng::seed_from_u64(n) da la misma secuencia en cada ejecución: genial para tests y niveles de juego repetibles, nunca para secretos.
  • Para claves y tokens, usa la fuente aleatoria del sistema operativo (getrandom) o un crate de criptografía especializado.

Comprueba lo aprendido

1. ¿Qué llamada puede devolver 6?

2. Con rand 0.10, rng.random_range(..) falla con "no method named random_range". ¿Qué falta?

3. ¿Para qué usarías StdRng::seed_from_u64(42)?

4. ¿Qué devuelve choose cuando lo llamas sobre un slice vacío?

Puntuación: 0 / 4