コンテンツにスキップ

Rustの乱数(rand)

ゲームではサイコロを振ります。クイズは問題の順番をシャッフルします。テストにはサンプルデータが必要です。どれも乱数(random number)が必要ですが、Rust の標準ライブラリには、すぐに使える乱数生成器がありません。その代わり、ほとんどの人が1つのクレートを使います。それが rand(英語)です。

このページでは、最新の rand 0.10 の API を紹介します。これは大事なポイントです。rand はバージョン 0.9 と 0.10 で、いくつかの名前を変えたからです。古いチュートリアルに thread_rng() や gen_range() が出てきたら、それは古いバージョン向けに書かれたものです。このページでは新しい名前と、古い名前を使ったときに出るエラーを見ていきます。

プロジェクトに rand を追加する

Section titled “プロジェクトに rand を追加する”

プロジェクトのフォルダで、次を実行します。

Terminal window
cargo add rand

すると、Cargo.toml に次のような行が追加されます。

[dependencies]
rand = "0.10"

クレートや Cargo.toml が初めてなら、先にそちらのページを読んでください。このページの「Try it」ボタンは Rust Playground を開きます。Playground には最初から rand が入っています。

ランダムな値が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 はどの型が欲しいのかわからないことがあります。

ランダムな値がたくさん必要なときは、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: true
x は 0.0..1.0 の範囲: true
▶ 実際に試してみよう

数そのものではなくチェックの結果を表示しているので、値は毎回違っても、実行結果は毎回同じになります。

よく使うメソッドを、あと2つ紹介します。

let lucky = rng.random_bool(0.1); // だいたい10%の確率で true
let rare = rng.random_ratio(1, 100); // だいたい100回に1回 true

use rand::RngExt; の行はなぜ必要なのでしょうか? random_range や random_bool のようなメソッドはトレイトから来ていて、トレイトのメソッドはそのトレイトがスコープに入っているときしか呼べないからです。rand 0.10 には、トレイトが2つあります。

  • Rng は「ランダムなビットを作れる」という基本のトレイトです。すべての生成器がこれを実装しています。
  • RngExt は、random、random_range、random_bool などの便利なメソッドを追加します。どの Rng でも自動的に使えます。

つまり、メソッドを呼ぶには RngExt をインポートし、「どんな生成器でも受け取る」関数を書くときは Rng を使います。下のサイコロの例では、両方が出てきます。

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]
同じ? true
▶ 実際に試してみよう

seed_from_u64 は SeedableRng トレイトのメソッドなので、これもインポートが必要です。2024 を別の数に変えると違う並びになりますが、それでも毎回同じ並びになります。

正直に1つ注意しておきます。StdRng のアルゴリズムは、将来の rand のリリースで変わる可能性があります。上の数は rand 0.10 のものです。テストで永遠にまったく同じ数が必要なら、rand の公式ガイドは ChaCha8Rng のような名前の決まった生成器を勧めています(下の「さらに詳しく」を見てください)。

リストを扱うには、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"]
空の配列から: None
▶ 実際に試してみよう

choose は Option を返します。空のスライスからは何も選べないからです。これらのメソッドは、配列、ベクタ、スライスのどれでも同じように使えます。シード付きの rng を rand::rng() に置き換えると、実行するたびに新しいシャッフルになります。

サイコロを振る関数を書いてみましょう。ポイントは引数の型です。&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] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ 実際に試してみよう

2d6 は、ボードゲームでよく使う「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
使ってよい文字だけ: true
▶ 実際に試してみよう

b"..." はバイト文字列で、ASCII 文字の &[u8] です。choose が1バイトを選び、as char でそれを文字に変えます。ここでは rand::rng() を使っていて、シード付きの生成器は決して使っていない点に注目してください。決まったシードから作ったパスワードは毎回同じパスワードになってしまい、パスワードとしての意味がまったくありません。

数当てゲームのプロジェクトでは、標準ライブラリだけで済むように、時計を使ったトリックで秘密の数を選んでいます。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() がまさにぴったりです。

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 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() になりました。

範囲が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)か専用の暗号クレートを使いましょう。

理解度チェック

1. 6 を返す可能性があるのはどれですか?

2. rand 0.10 で rng.random_range(..) が "no method named random_range" というエラーになります。何が足りませんか?

3. StdRng::seed_from_u64(42) を使うのはなぜですか?

4. 空のスライスで choose を呼ぶと、何が返りますか?

スコア: 0 / 4