Перейти к содержимому

Случайные числа в Rust с крейтом rand

Игры бросают кубики. Викторины перемешивают вопросы. Тестам нужны данные для проверки. Всему этому нужны случайные числа, а в стандартной библиотеке Rust нет готового генератора случайных чисел. Вместо этого почти все пользуются одним крейтом: rand (на английском).

На этой странице показан современный API rand 0.10. Это важно, потому что в версиях 0.9 и 0.10 rand переименовал некоторые функции. Если в старом руководстве встречаются thread_rng() или gen_range(), значит, оно написано для старой версии. Мы посмотрим на новые имена и на ошибки, которые выдают старые.

В папке проекта выполни:

Terminal window
cargo add rand

Эта команда добавит в Cargo.toml примерно такую строку:

[dependencies]
rand = "0.10"

Если крейты и Cargo.toml для тебя пока в новинку, сначала прочитай эту страницу. Кнопки «Попробуй сам» на этой странице открывают Rust Playground, где rand уже есть.

Для одного случайного значения используй свободные функции rand::random_range и rand::random:

fn main() {
let die: u32 = rand::random_range(1..=6);
let coin: bool = rand::random();
let chance: f64 = rand::random();
println!("на кубике от 1 до 6: {}", (1..=6).contains(&die));
println!("chance в 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("выпал орёл: {coin}");
}
▶ Попробуй сам

Блока с выводом здесь нет специально: последняя строка случайно оказывается true или false. Нажми «Попробуй сам» несколько раз и посмотри, как она меняется.

  • random_range(1..=6) даёт число от 1 до 6, включая 6. С 1..6 ты получишь только числа от 1 до 5. Диапазоны работают так же, как в цикле for.
  • random() выбирает значение того типа, который ты попросишь. Для bool это честное подбрасывание монетки. Для f64 — число от 0.0 до 1.0 (не включая 1.0).
  • Аннотация типа (: u32, : bool) говорит rand, что именно создать. Без неё Rust может не понять, какой тип тебе нужен.

Когда нужно много случайных значений, один раз возьми генератор через rand::rng() и вызывай у него методы. Так чуть быстрее, и именно этот стиль ты увидишь в большинстве кода:

use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect();
println!("брошено кубиков: {}", rolls.len());
println!("все от 1 до 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x в 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Вывод:

брошено кубиков: 1000
все от 1 до 6: true
x в 0.0..1.0: true
▶ Попробуй сам

Мы печатаем проверки, а не сами числа, поэтому вывод каждый раз одинаковый, хотя значения разные.

Ещё два метода, которые тебе часто пригодятся:

let lucky = rng.random_bool(0.1); // true примерно в 10% случаев
let rare = rng.random_ratio(1, 100); // true примерно 1 раз из 100

Зачем строка use rand::RngExt;? Методы вроде random_range и random_bool приходят из трейта, а методы трейта можно вызывать, только когда трейт находится в области видимости. В rand 0.10 есть два трейта:

  • Rng — базовый трейт «я умею выдавать случайные биты». Его реализует каждый генератор.
  • RngExt добавляет удобные методы: random, random_range, random_bool и другие. Он автоматически работает для любого Rng.

Итак: импортируй RngExt, чтобы вызывать методы, а Rng используй, когда пишешь функцию, которая принимает «любой генератор». Оба трейта встретятся в примере с кубиками ниже.

rand::rng() при каждом запуске выдаёт разные числа. В настоящей игре так и нужно. Но для тестов, отладки или «ежедневного задания» в игре хочется получать одни и те же «случайные» числа каждый раз.

Для этого есть seed (начальное значение, «зерно»). Генератор с seed всегда выдаёт одну и ту же последовательность для одного и того же 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!("одинаковые? {}", first == second);
}

Вывод:

[53, 82, 74, 51]
[53, 82, 74, 51]
одинаковые? true
▶ Попробуй сам

seed_from_u64 приходит из трейта SeedableRng, поэтому его тоже нужно импортировать. Замени 2024 на другое число — и получишь другую последовательность, но опять одну и ту же при каждом запуске.

Одно честное предупреждение: алгоритм StdRng может измениться в будущей версии rand. Числа выше получены с rand 0.10. Если твои тесты должны давать в точности те же числа всегда, книга rand советует генератор с конкретным именем, например ChaCha8Rng (см. «Что почитать» ниже).

Чтобы работать со списками, импортируй ещё два трейта из rand::seq:

  • SliceRandom даёт shuffle, который перемешивает срез на месте.
  • IndexedRandom даёт choose (один случайный элемент) и sample (несколько разных элементов).
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!("перемешано: {:?}", cards);
let colors = ["красный", "зелёный", "синий"];
println!("выбран: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("команда: {:?}", team);
let empty: [i32; 0] = [];
println!("из пустого: {:?}", empty.choose(&mut rng));
}

Вывод:

перемешано: ["K", "J", "10", "Q", "A"]
выбран: Some("красный")
команда: ["K", "J"]
из пустого: None
▶ Попробуй сам

choose возвращает Option, потому что из пустого среза выбирать нечего. Эти методы одинаково работают с массивами, векторами и срезами. Замени rng с seed на rand::rng() — и при каждом запуске будет новое перемешивание.

Напишем функцию, которая бросает кубики. Вся хитрость в типе параметра: &mut impl Rng означает «любой генератор случайных чисел». В настоящей игре ты передаёшь rand::rng(). В тесте — StdRng с seed и точно знаешь, что получится:

use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
/// Бросает `count` кубиков, у каждого `sides` граней.
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);
}
}

Вывод:

2d6: [5, 6] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ Попробуй сам

2d6 — это принятое в настольных играх сокращение для «двух шестигранных кубиков». Передавать генератор в функцию, а не вызывать rand::rng() внутри roll, — хорошая привычка: так функцию легко тестировать.

Здесь мы выбираем 16 символов из разрешённого набора. В наборе нет похожих друг на друга символов вроде 0/O и 1/l, так что пароль проще набирать:

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!("длина: {}", pw.len());
println!("только разрешённые символы: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // раскомментируй, чтобы увидеть: при каждом запуске он разный
}

Вывод:

длина: 16
только разрешённые символы: true
▶ Попробуй сам

b"..." — это байтовая строка: &[u8] из символов ASCII. choose выбирает один байт, а as char превращает его в символ. Обрати внимание: здесь мы взяли rand::rng() и ни в коем случае не генератор с seed. Пароль из фиксированного seed каждый раз один и тот же, а значит, это вовсе не пароль.

Проект «Угадай число» выбирает секретное число с помощью трюка с часами, чтобы обойтись одной стандартной библиотекой. С rand вся функция secret_number превращается в одну строку:

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

Всё остальное в игре остаётся прежним. Вот версия, которую можно запустить в Playground. Секрет берётся из генератора с seed, а компьютер угадывает, каждый раз выбирая середину оставшегося диапазона:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// В настоящей игре: 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} — слишком мало"); low = guess + 1; }
Ordering::Greater => { println!("{guess} — слишком много"); high = guess - 1; }
Ordering::Equal => { println!("{guess} — верно! Найдено с {attempts}-й попытки."); break; }
}
}
}

Вывод:

50 — слишком мало
75 — слишком мало
88 — слишком мало
94 — слишком мало
97 — слишком мало
99 — верно! Найдено с 6-й попытки.
▶ Попробуй сам

Забавный факт: стратегии «дели диапазон пополам» никогда не нужно больше 7 попыток для чисел от 1 до 100.

Короткий ответ: rand::rng() спроектирован так, чтобы его было трудно предсказать, но для серьёзных секретов бери инструменты, созданные специально для этого.

  • rand::rng() использует криптографически стойкий алгоритм (сейчас это ChaCha12). Он получает seed из источника случайности операционной системы через крейт getrandom (на английском) и регулярно обновляет его. Документация rand называет его «быстрым, достаточно надёжным генератором» и говорит, что не может обещать, что он подойдёт для любых задач безопасности.
  • Для ключей шифрования, API-токенов и других важных секретов бери байты прямо у операционной системы через getrandom (или rand::rngs::SysRng), а ещё лучше — пусть ключ сгенерирует специальная криптографическая библиотека.
  • Никогда не используй генератор с seed (seed_from_u64) или быстрый SmallRng для чего-то секретного. Любой, кто знает или угадает seed, получит те же «случайные» значения.

Для игр, симуляций, перемешивания и тестовых данных rand::rng() подходит идеально.

Импортировать Rng вместо RngExt. В rand 0.9 методы жили в Rng, поэтому старый код пишет use rand::Rng;. С rand 0.10 это даёт:

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;
|

Компилятор сообщает, что у ThreadRng нет метода random_range, и сам подсказывает решение: use rand::RngExt;.

Копировать код, написанный для rand 0.8. В совсем старых примерах встречаются thread_rng() и 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`

Компилятор не находит в крейте rand функцию thread_rng. Новые имена — rand::rng() и random_range(). Точно так же gen() стал random(), а gen_bool() — random_bool().

Промах на единицу в диапазоне. random_range(1..6) никогда не вернёт 6. Для кубика пиши 1..=6.

  • Добавь крейт через cargo add rand: своего генератора случайных чисел в стандартной библиотеке Rust нет.
  • Для быстрых значений используй rand::random_range(1..=6) и rand::random::<T>().
  • Для множества значений бери let mut rng = rand::rng(); и use rand::RngExt; ради методов вроде random_range и random_bool.
  • SliceRandom::shuffle перемешивает срез; IndexedRandom::choose выбирает один элемент и возвращает Option.
  • StdRng::seed_from_u64(n) при каждом запуске даёт одну и ту же последовательность: отлично для тестов и повторяемых игровых уровней, но никогда — для секретов.
  • Для ключей и токенов используй источник случайности ОС (getrandom) или специальный криптографический крейт.

Проверь себя

1. Какой вызов может вернуть 6?

2. С rand 0.10 вызов rng.random_range(..) падает с ошибкой «no method named random_range». Чего не хватает?

3. Зачем использовать StdRng::seed_from_u64(42)?

4. Что вернёт choose, если вызвать его на пустом срезе?

Результат: 0 / 4

  • The Rust Rand Book (на английском): официальное руководство по rand, от быстрого старта до выбора генератора.
  • Документация API rand — docs.rs (на английском): все функции, трейты и генераторы, с примерами.
  • Updating to 0.10 — The Rust Rand Book (на английском): что изменилось в 0.10, включая переименование Rng/RngExt. Пригодится, когда встретишь старый код.
  • Our RNGs — The Rust Rand Book (на английском): сравнение генераторов, в том числе какие из них подходят для задач безопасности.
  • Reproducibility — The Rust Rand Book (на английском): как сделать результаты с seed стабильными между версиями и платформами.