Pular para o conteúdo

Números aleatórios em Rust com rand

Jogos rolam dados. Quizzes embaralham as perguntas. Testes precisam de dados de exemplo. Tudo isso precisa de números aleatórios, e a biblioteca padrão do Rust não tem um gerador de números aleatórios pronto. Em vez disso, quase todo mundo usa um crate (pacote): o rand (em inglês).

Esta página mostra a API moderna do rand 0.10. Isso importa, porque o rand mudou alguns nomes nas versões 0.9 e 0.10. Se um tutorial mais antigo mostra thread_rng() ou gen_range(), ele foi escrito para uma versão anterior. Vamos ver os nomes novos e os erros que os nomes antigos causam.

Na pasta do seu projeto, rode:

Terminal window
cargo add rand

Isso acrescenta uma linha como esta ao seu Cargo.toml:

[dependencies]
rand = "0.10"

Se crates e o Cargo.toml são novidade para você, leia essa página antes. Os botões “Experimente você mesmo” desta página abrem o Rust Playground, que já inclui o rand.

Para um valor aleatório avulso, use as funções livres 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!("o dado está entre 1 e 6: {}", (1..=6).contains(&die));
println!("chance está em 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("a moeda deu cara: {coin}");
}
▶ Experimente você mesmo

Não tem bloco de saída aqui de propósito: a última linha é true ou false ao acaso. Clique em “Experimente você mesmo” algumas vezes e veja mudar.

  • random_range(1..=6) dá um número de 1 a 6, incluindo o 6. Com 1..6 você só teria de 1 a 5. Os intervalos funcionam igual aos de um laço for.
  • random() escolhe um valor do tipo que você pedir. Para um bool, é um cara ou coroa justo. Para um f64, é um número de 0.0 até 1.0 (sem incluir o 1.0).
  • A anotação de tipo (: u32, : bool) diz ao rand o que gerar. Sem ela, o Rust pode não saber qual tipo você quer.

Quando você precisa de muitos valores aleatórios, pegue um gerador uma vez com rand::rng() e chame métodos nele. Isso é um pouco mais rápido, e é o estilo que você vai ver na maior parte do 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!("rolou {} dados", rolls.len());
println!("todos entre 1 e 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x está em 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Saída:

rolou 1000 dados
todos entre 1 e 6: true
x está em 0.0..1.0: true
▶ Experimente você mesmo

Imprimimos verificações em vez dos próprios números, para que a saída seja sempre a mesma, mesmo que os valores não sejam.

Mais dois métodos que você vai usar bastante:

let lucky = rng.random_bool(0.1); // true em cerca de 10% das vezes
let rare = rng.random_ratio(1, 100); // true em cerca de 1 vez a cada 100

Por que a linha use rand::RngExt;? Métodos como random_range e random_bool vêm de uma trait, e você só pode chamar os métodos de uma trait quando ela está no escopo. No rand 0.10 existem duas traits:

  • Rng é a trait básica de “eu sei produzir bits aleatórios”. Todo gerador a implementa.
  • RngExt acrescenta os métodos práticos: random, random_range, random_bool e outros. Ela funciona automaticamente em todo Rng.

Então: importe RngExt para chamar os métodos, e use Rng quando escrever uma função que aceita “qualquer gerador”. Você vai ver as duas no rolador de dados mais abaixo.

rand::rng() dá números diferentes a cada execução. É isso que você quer em um jogo de verdade. Mas, para testes, depuração ou a fase de “desafio do dia” de um jogo, você quer os mesmos números “aleatórios” toda vez.

Uma seed (semente) faz isso. Um gerador com seed sempre produz a mesma sequência para a mesma 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!("iguais? {}", first == second);
}

Saída:

[53, 82, 74, 51]
[53, 82, 74, 51]
iguais? true
▶ Experimente você mesmo

seed_from_u64 vem da trait SeedableRng, então ela também precisa ser importada. Troque 2024 por outro número e você ganha uma sequência diferente, mas ainda a mesma em todas as execuções.

Um aviso sincero: o algoritmo do StdRng pode mudar em uma versão futura do rand. Os números acima vêm do rand 0.10. Se os seus testes precisam dar exatamente os mesmos números para sempre, o livro do rand recomenda um gerador com nome fixo, como o ChaCha8Rng (veja “Para saber mais” abaixo).

Para trabalhar com listas, importe mais duas traits de rand::seq:

  • SliceRandom dá o shuffle, que embaralha uma slice (fatia) no próprio lugar.
  • IndexedRandom dá o choose (um item aleatório) e o sample (vários itens diferentes).
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!("embaralhadas: {:?}", cards);
let colors = ["vermelho", "verde", "azul"];
println!("escolhida: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("time: {:?}", team);
let empty: [i32; 0] = [];
println!("de uma vazia: {:?}", empty.choose(&mut rng));
}

Saída:

embaralhadas: ["K", "J", "10", "Q", "A"]
escolhida: Some("vermelho")
time: ["K", "J"]
de uma vazia: None
▶ Experimente você mesmo

choose devolve um Option, porque uma slice vazia não tem nada para escolher. Esses métodos funcionam do mesmo jeito em arrays, vetores e slices. Troque o rng com seed por rand::rng() e você ganha um embaralhamento novo a cada execução.

Vamos escrever uma função que rola dados. O segredo está no tipo do parâmetro: &mut impl Rng significa “qualquer gerador aleatório”. No jogo de verdade, você passa rand::rng(). Em um teste, você passa um StdRng com seed e sabe exatamente o que vai sair:

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

Saída:

2d6: [5, 6] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ Experimente você mesmo

2d6 é a abreviação de jogos de tabuleiro e RPG para “dois dados de seis lados”. Passar o gerador como parâmetro, em vez de chamar rand::rng() dentro de roll, é um bom hábito: deixa a função fácil de testar.

Aqui escolhemos 16 caracteres de um conjunto permitido. O conjunto deixa de fora caracteres parecidos, como 0/O e 1/l, para que a senha seja mais fácil de digitar:

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!("tamanho: {}", pw.len());
println!("só caracteres permitidos: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // tire o comentário para ver: muda a cada execução
}

Saída:

tamanho: 16
só caracteres permitidos: true
▶ Experimente você mesmo

b"..." é uma byte string: um &[u8] de caracteres ASCII. choose escolhe um byte, e as char o transforma em um caractere. Repare que usamos rand::rng() aqui, e nunca um gerador com seed. Uma senha gerada a partir de uma seed fixa é a mesma senha toda vez, ou seja, não é senha nenhuma.

O projeto do Jogo de adivinhação escolhe o número secreto com um truque usando o relógio, para continuar usando só a biblioteca padrão. Com o rand, a função secret_number inteira vira uma linha:

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

Todo o resto do jogo continua igual. Aqui vai uma versão que você pode rodar no Playground. O número secreto vem de um gerador com seed, e o computador tenta adivinhar sempre escolhendo o meio do intervalo que sobrou:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// No jogo de verdade: 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} é pequeno demais"); low = guess + 1; }
Ordering::Greater => { println!("{guess} é grande demais"); high = guess - 1; }
Ordering::Equal => { println!("{guess} está certo! Encontrado em {attempts} tentativas."); break; }
}
}
}

Saída:

50 é pequeno demais
75 é pequeno demais
88 é pequeno demais
94 é pequeno demais
97 é pequeno demais
99 está certo! Encontrado em 6 tentativas.
▶ Experimente você mesmo

Curiosidade: essa estratégia de “dividir o intervalo ao meio” nunca precisa de mais de 7 palpites para números de 1 a 100.

Resposta curta: rand::rng() foi feito para ser difícil de prever, mas, para segredos sérios, use ferramentas feitas para isso.

  • rand::rng() usa um algoritmo criptograficamente seguro (atualmente o ChaCha12). Ele recebe a seed da fonte de aleatoriedade do seu sistema operacional, por meio do crate getrandom (em inglês), e troca a própria seed regularmente. A documentação do rand o chama de “gerador rápido e razoavelmente seguro”, e diz que não pode prometer que ele atende a todas as necessidades de segurança.
  • Para chaves de criptografia, tokens de API e outros segredos importantes, pegue os bytes direto do sistema operacional com o getrandom (ou rand::rngs::SysRng) ou, melhor ainda, deixe uma biblioteca de criptografia dedicada gerar a chave para você.
  • Nunca use um gerador com seed (seed_from_u64) ou o rápido SmallRng para algo secreto. Qualquer pessoa que saiba ou adivinhe a seed consegue produzir os mesmos valores “aleatórios”.

Para jogos, simulações, embaralhamentos e dados de teste, rand::rng() é exatamente o certo.

Importar Rng em vez de RngExt. No rand 0.9 os métodos ficavam em Rng, então código mais antigo faz use rand::Rng;. Com o rand 0.10, isso 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 scope
help: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it
|
1 + use rand::RngExt;
|

O compilador diz que não encontrou o método random_range, explica que métodos de traits só podem ser usados quando a trait está no escopo, e já mostra a solução: use rand::RngExt;.

Copiar código escrito para o rand 0.8. Exemplos bem antigos usam 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`

O erro diz que a função thread_rng não existe no crate rand. Os nomes novos são rand::rng() e random_range(). Da mesma forma, gen() virou random() e gen_bool() virou random_bool().

Intervalos com um a menos. random_range(1..6) nunca devolve 6. Para um dado, escreva 1..=6.

  • Adicione o crate com cargo add rand. A biblioteca padrão do Rust não tem um gerador de números aleatórios próprio.
  • Para valores rápidos, use rand::random_range(1..=6) e rand::random::<T>().
  • Para muitos valores, use let mut rng = rand::rng(); com use rand::RngExt; para métodos como random_range e random_bool.
  • SliceRandom::shuffle embaralha uma slice; IndexedRandom::choose escolhe um item e devolve um Option.
  • StdRng::seed_from_u64(n) dá a mesma sequência em todas as execuções: ótimo para testes e fases de jogo repetíveis, nunca para segredos.
  • Para chaves e tokens, use a fonte de aleatoriedade do sistema operacional (getrandom) ou um crate de criptografia dedicado.

Teste o que aprendeu

1. Qual chamada pode devolver 6?

2. Com o rand 0.10, rng.random_range(..) falha com "no method named random_range". O que está faltando?

3. Por que você usaria StdRng::seed_from_u64(42)?

4. O que choose devolve quando você o chama em uma slice vazia?

Pontuação: 0 / 4