Rust 随机数
游戏要掷骰子,测验要打乱题目顺序,测试需要样例数据。这些都离不开随机数,而 Rust 的标准库并没有现成的随机数生成器。几乎所有人都会用同一个 crate:rand(英文)。
这一页介绍的是新版 rand 0.10 的 API。这一点很重要,因为 rand 在 0.9 和 0.10 版本里改了一些名字。如果某篇旧教程里出现了 thread_rng() 或 gen_range(),说明它是为旧版本写的。下面会介绍新名字,也会看看用旧名字时会报什么错。
把 rand 加进项目
Section titled “把 rand 加进项目”在项目文件夹里运行:
cargo add rand它会在 Cargo.toml 里加上这样一行:
[dependencies]rand = "0.10"如果你还不熟悉 crate 和 Cargo.toml,请先读那一页。这一页的“Try it”按钮会打开 Rust Playground,那里已经自带了 rand。
快速得到一个随机值
Section titled “快速得到一个随机值”只想要一个随机值时,用自由函数 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。多点几次“Try it”,看看它怎么变。
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 trait
Section titled “生成器和 RngExt trait”需要很多随机值时,先用 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); // 大约 10% 的概率为 truelet rare = rng.random_ratio(1, 100); // 大约每 100 次有 1 次为 true为什么要写 use rand::RngExt; 这一行?random_range 和 random_bool 这样的方法来自一个 trait,只有当这个 trait 在作用域里时,才能调用它的方法。rand 0.10 里有两个 trait:
Rng是最基础的“我能产生随机位”的 trait,每个生成器都实现了它。RngExt加上了好用的方法:random、random_range、random_bool等等。它自动适用于所有Rng。
所以:要调用这些方法就导入 RngExt;写一个接收“任意生成器”的函数时就用 Rng。后面的掷骰子程序里,这两个都会用到。
用种子得到可复现的结果
Section titled “用种子得到可复现的结果”rand::rng() 每次运行都会给出不同的数字。在真正的游戏里,这正是你想要的。但在测试、调试或者游戏的“每日挑战”关卡里,你会希望每次都得到相同的“随机”数。
种子(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 trait,所以它也要导入。把 2024 换成别的数,会得到另一个序列,但每次运行仍然是同一个。
说句实话:StdRng 用的算法在将来的 rand 版本里可能会变。上面的数字来自 rand 0.10。如果你的测试必须永远得到完全相同的数字,rand 官方手册推荐使用有具体名字的生成器,比如 ChaCha8Rng(见下面的“延伸阅读”)。
打乱 slice 和从中挑选
Section titled “打乱 slice 和从中挑选”要处理列表,需要再从 rand::seq 导入两个 trait:
SliceRandom提供shuffle,原地打乱一个 slice。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 = ["red", "green", "blue"]; 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("red")队伍:["K", "J"]从空数组里选:Nonechoose 返回一个 Option,因为空 slice 里没东西可选。这些方法对数组、vector 和 slice 都适用。把带种子的 rng 换成 rand::rng(),每次运行就会得到新的打乱结果。
示例:掷骰子程序
Section titled “示例:掷骰子程序”来写一个掷骰子的函数。关键在参数类型:&mut impl Rng 的意思是“任意随机数生成器”。在真正的游戏里传入 rand::rng();在测试里传入带种子的 StdRng,就能确切知道会得到什么:
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 是桌游里的简写,意思是“两个六面骰”。把生成器作为参数传进去,而不是在 roll 里面调用 rand::rng(),是个好习惯:这样函数很容易测试。
示例:随机密码生成器
Section titled “示例:随机密码生成器”这里从一组允许的字符里挑出 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"..." 是字节字符串:一个由 ASCII 字符组成的 &[u8]。choose 挑出一个字节,as char 把它变成字符。注意这里用的是 rand::rng(),绝对不要用带种子的生成器。用固定种子生成的密码每次都一样,那就根本不算密码了。
回到猜数字游戏
Section titled “回到猜数字游戏”猜数字游戏项目用一个时钟小技巧来生成秘密数字,这样就只需要标准库。有了 rand,整个 secret_number 函数只要一行:
let secret: u32 = rand::random_range(1..=100);游戏的其他部分都不用改。下面是一个能在 Playground 里运行的版本。秘密数字来自带种子的生成器,电脑每次都猜剩余范围的正中间:
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 次。小知识:这种“每次把范围砍一半”的策略,在 1 到 100 之间最多只需要猜 7 次。
rand 能用来生成密码和密钥吗?
Section titled “rand 能用来生成密码和密钥吗?”简短的回答:rand::rng() 的设计目标就是难以预测,但对于真正重要的机密,请用专门干这件事的工具。
rand::rng()使用一种密码学安全的算法(目前是 ChaCha12)。它通过getrandom(英文)crate 从操作系统的随机源获取种子,并定期重新播种。rand 文档称它是“快速、相当安全的生成器”,并表示无法保证它满足所有安全需求。- 对于加密密钥、API 令牌以及其他高风险的机密,直接用
getrandom(或rand::rngs::SysRng)从操作系统获取字节;更好的做法是让专门的密码学库替你生成密钥。 - 绝对不要把带种子的生成器(
seed_from_u64)或快速的SmallRng用在任何机密上。知道或猜出种子的人,都能生成同样的“随机”值。
对于游戏、模拟、洗牌和测试数据,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 方法,因为提供它的 trait RngExt 不在作用域里。编译器已经告诉你怎么修复了: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添加这个 crate。Rust 标准库自己没有随机数生成器。 - 快速取值用
rand::random_range(1..=6)和rand::random::<T>()。 - 需要很多值时,用
let mut rng = rand::rng();,再加上use rand::RngExt;,就能调用random_range、random_bool等方法。 SliceRandom::shuffle打乱一个 slice;IndexedRandom::choose挑出一个元素,返回Option。StdRng::seed_from_u64(n)每次运行都给出相同的序列:非常适合测试和可重玩的游戏关卡,但绝不能用于机密。- 密钥和令牌请用操作系统的随机源(
getrandom)或专门的密码学 crate。
小测验
1. 哪个调用可能返回 6?
2. 在 rand 0.10 里,rng.random_range(..) 报错“no method named random_range”。缺了什么?
3. 为什么要用 StdRng::seed_from_u64(42)?
4. 在空 slice 上调用 choose 会返回什么?
得分: 0 / 4
- The Rust Rand Book(英文):
rand的官方手册,从快速入门讲到如何选择生成器。 randAPI 文档 — docs.rs(英文):所有函数、trait 和生成器,都附有示例。- Updating to 0.10 — The Rust Rand Book(英文):0.10 版本有哪些变化,包括
Rng/RngExt的改名。遇到旧代码时很有用。 - Our RNGs — The Rust Rand Book(英文):比较各种生成器,包括哪些适合安全用途。
- Reproducibility — The Rust Rand Book(英文):如何让带种子的结果在不同版本和平台上保持稳定。