Случайные числа в Rust с крейтом rand
Игры бросают кубики. Викторины перемешивают вопросы. Тестам нужны данные для проверки. Всему этому нужны случайные числа, а в стандартной библиотеке Rust нет готового генератора случайных чисел. Вместо этого почти все пользуются одним крейтом: rand (на английском).
На этой странице показан современный API rand 0.10. Это важно, потому что в версиях 0.9 и 0.10 rand переименовал некоторые функции. Если в старом руководстве встречаются thread_rng() или gen_range(), значит, оно написано для старой версии. Мы посмотрим на новые имена и на ошибки, которые выдают старые.
Добавляем rand в проект
Заголовок раздела «Добавляем rand в проект»В папке проекта выполни:
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 может не понять, какой тип тебе нужен.
Генератор и трейт RngExt
Заголовок раздела «Генератор и трейт RngExt»Когда нужно много случайных значений, один раз возьми генератор через 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: truex в 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 используй, когда пишешь функцию, которая принимает «любой генератор». Оба трейта встретятся в примере с кубиками ниже.
Повторяемые результаты с seed
Заголовок раздела «Повторяемые результаты с seed»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]одинаковые? trueseed_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"]из пустого: Nonechoose возвращает 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] = 111d20: [16] = 163d4: [3, 1, 2] = 62d6 — это принятое в настольных играх сокращение для «двух шестигранных кубиков». Передавать генератор в функцию, а не вызывать 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только разрешённые символы: trueb"..." — это байтовая строка: &[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 для паролей и ключей?
Заголовок раздела «Подходит ли rand для паролей и ключей?»Короткий ответ: 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 scopehelp: 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 стабильными между версиями и платформами.