Rustの乱数(rand)
ゲームではサイコロを振ります。クイズは問題の順番をシャッフルします。テストにはサンプルデータが必要です。どれも乱数(random number)が必要ですが、Rust の標準ライブラリには、すぐに使える乱数生成器がありません。その代わり、ほとんどの人が1つのクレートを使います。それが 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"クレートや Cargo.toml が初めてなら、先にそちらのページを読んでください。このページの「Try it」ボタンは Rust Playground を開きます。Playground には最初から rand が入っています。
手軽にランダムな値を作る
Section titled “手軽にランダムな値を作る”ランダムな値が1つだけ欲しいときは、自由関数(free function)の 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 トレイト
Section titled “生成器と RngExt トレイト”ランダムな値がたくさん必要なときは、rand::rng() で生成器(generator)を一度だけ取り出して、そのメソッドを呼びます。こちらのほうが少し速く、ほとんどのコードで見かける書き方です。
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数そのものではなくチェックの結果を表示しているので、値は毎回違っても、実行結果は毎回同じになります。
よく使うメソッドを、あと2つ紹介します。
let lucky = rng.random_bool(0.1); // だいたい10%の確率で truelet rare = rng.random_ratio(1, 100); // だいたい100回に1回 trueuse rand::RngExt; の行はなぜ必要なのでしょうか? random_range や random_bool のようなメソッドはトレイトから来ていて、トレイトのメソッドはそのトレイトがスコープに入っているときしか呼べないからです。rand 0.10 には、トレイトが2つあります。
Rngは「ランダムなビットを作れる」という基本のトレイトです。すべての生成器がこれを実装しています。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 トレイトのメソッドなので、これもインポートが必要です。2024 を別の数に変えると違う並びになりますが、それでも毎回同じ並びになります。
正直に1つ注意しておきます。StdRng のアルゴリズムは、将来の rand のリリースで変わる可能性があります。上の数は rand 0.10 のものです。テストで永遠にまったく同じ数が必要なら、rand の公式ガイドは ChaCha8Rng のような名前の決まった生成器を勧めています(下の「さらに詳しく」を見てください)。
スライスのシャッフルと選択
Section titled “スライスのシャッフルと選択”リストを扱うには、rand::seq からトレイトをさらに2つインポートします。
SliceRandomは、スライスをその場でかき混ぜるshuffleを提供します。IndexedRandomは、choose(ランダムに1つ選ぶ)と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 を返します。空のスライスからは何も選べないからです。これらのメソッドは、配列、ベクタ、スライスのどれでも同じように使えます。シード付きの rng を rand::rng() に置き換えると、実行するたびに新しいシャッフルになります。
例:サイコロを振る
Section titled “例:サイコロを振る”サイコロを振る関数を書いてみましょう。ポイントは引数の型です。&mut impl Rng は「どんな乱数生成器でもいい」という意味です。本物のゲームでは rand::rng() を渡します。テストではシード付きの StdRng を渡せば、何が出てくるかが正確にわかります。
use rand::rngs::StdRng;use rand::{Rng, RngExt, SeedableRng};
/// `sides` 面のサイコロを `count` 個振る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 は、ボードゲームでよく使う「6面のサイコロを2個」という意味の書き方です。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 が1バイトを選び、as char でそれを文字に変えます。ここでは rand::rng() を使っていて、シード付きの生成器は決して使っていない点に注目してください。決まったシードから作ったパスワードは毎回同じパスワードになってしまい、パスワードとしての意味がまったくありません。
数当てゲームに戻る
Section titled “数当てゲームに戻る”数当てゲームのプロジェクトでは、標準ライブラリだけで済むように、時計を使ったトリックで秘密の数を選んでいます。rand を使えば、secret_number 関数全体がたった1行になります。
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(英語)クレートを通じて OS の乱数源から取得され、定期的に自分でシードを取り直します。rand のドキュメントはこれを「速くて、それなりに安全な生成器」と呼んでいて、あらゆるセキュリティの要件を満たすとまでは約束できない、としています。- 暗号鍵、API トークンなど、重要な秘密には、
getrandom(またはrand::rngs::SysRng)で OS から直接バイトを取得しましょう。もっといいのは、鍵の生成を専用の暗号ライブラリに任せることです。 - 秘密に関わるものには、シード付きの生成器(
seed_from_u64)や高速なSmallRngを決して使わないでください。シードを知っている人や推測できた人は、同じ「ランダムな」値を作れてしまいます。
ゲーム、シミュレーション、シャッフル、テストデータには、rand::rng() がまさにぴったりです。
よくある間違い
Section titled “よくある間違い”RngExt ではなく Rng をインポートする。 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() になりました。
範囲が1つずれる。 random_range(1..6) が 6 を返すことはありません。サイコロなら 1..=6 と書きましょう。
cargo add randでクレートを追加します。Rust の標準ライブラリには、乱数生成器そのものはありません。- 手軽な値なら
rand::random_range(1..=6)やrand::random::<T>()を使います。 - たくさんの値が必要なら
let mut rng = rand::rng();を使い、random_rangeやrandom_boolのようなメソッドのためにuse rand::RngExt;を書きます。 SliceRandom::shuffleはスライスをかき混ぜ、IndexedRandom::chooseは要素を1つ選んでOptionを返します。StdRng::seed_from_u64(n)は毎回同じ並びを返します。テストや再現できるゲームのステージにはぴったりですが、秘密には決して使いません。- 鍵やトークンには、OS の乱数源(
getrandom)か専用の暗号クレートを使いましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. 6 を返す可能性があるのはどれですか?
2. rand 0.10 で rng.random_range(..) が "no method named random_range" というエラーになります。何が足りませんか?
3. StdRng::seed_from_u64(42) を使うのはなぜですか?
4. 空のスライスで choose を呼ぶと、何が返りますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- The Rust Rand Book(英語):
randの公式ガイドです。クイックスタートから生成器の選び方まで。 randの API ドキュメント — docs.rs(英語):すべての関数、トレイト、生成器が例と一緒に載っています。- Updating to 0.10 — The Rust Rand Book(英語):
Rng/RngExtの名前の変更を含む、0.10 での変更点です。古いコードに出会ったときに役立ちます。 - Our RNGs — The Rust Rand Book(英語):生成器を比べて、どれがセキュリティ用途に向いているかを説明しています。
- Reproducibility — The Rust Rand Book(英語):シード付きの結果を、バージョンやプラットフォームが違っても変わらないようにする方法です。