C#でアプリケーションを開発する際、乱数生成はゲームのロジック、シミュレーション、テスティング、セキュリティトークンの発行など、非常に幅広い場面で利用されます。
しかし、C#の乱数生成には複数の手法があり、用途に応じて適切なクラスを選択しなければ、意図しない挙動や脆弱性を招く恐れがあります。
かつての .NET 開発では System.Random クラスのインスタンス管理に細心の注意を払う必要がありましたが、近年の .NET アップデートにより、より安全で高機能な API が提供されるようになりました。
本記事では、2026年現在の最新の C# 環境(.NET 10以降を見据えた標準的な実装)に基づき、基本からスレッドセーフな実装、さらにはセキュリティを考慮した乱数生成までを詳しく解説します。
System.Randomクラスの基本操作
C#で最も一般的に使用される乱数生成器が System.Random クラスです。
これは疑似乱数ジェネレータ(PRNG)と呼ばれ、特定のアルゴリズムに基づいて数値の列を生成します。
インスタンスの生成と基本的なメソッド
最もシンプルな使い方は、Random クラスをインスタンス化し、Next メソッドを呼び出すことです。
// Randomクラスのインスタンス化
Random rand = new Random();
// 0以上の整数を取得
int num1 = rand.Next();
// 0以上100未満の整数を取得
int num2 = rand.Next(100);
// 10以上50未満の整数を取得
int num3 = rand.Next(10, 50);
// 0.0以上1.0未満の浮動小数点数を取得
double dbl = rand.NextDouble();
Console.WriteLine($"整数1: {num1}");
Console.WriteLine($"100未満: {num2}");
Console.WriteLine($"10~50未満: {num3}");
Console.WriteLine($"実数: {dbl}");
整数1: 1572849302
100未満: 42
10~50未満: 27
実数: 0.8429174
浮動小数点数の高度な生成
近年の .NET では、float 型(Single)を直接生成する NextSingle() メソッドも追加されています。
これにより、メモリ効率や計算速度を重視するゲーム開発などにおいて、不要な型変換を避けることが可能です。
Random rand = new Random();
// float型の乱数を生成
float f = rand.NextSingle();
Console.WriteLine($"float型の値: {f}");
従来の Random クラスにおける注意点
C#の学習者が最初につまずきやすいポイントとして、Random インスタンスの生成タイミングがあります。
ループ内での生成による重複問題
以下のようなコードは、初心者が陥りやすい典型的なアンチパターンです。
for (int i = 0; i < 5; i++)
{
// ループの中で毎回インスタンスを作ってしまう
Random badRand = new Random();
Console.WriteLine(badRand.Next(1, 100));
}
このコードを実行すると、多くの場合で「すべて同じ数値」が出力されます。
これは、Random のデフォルトのシード値がシステムの時刻に基づいているためです。
コンピュータの処理速度は非常に速いため、同一ミリ秒内に生成されたインスタンスは同じシード値を持ち、結果として同じ乱数列を返してしまいます。
これを避けるためには、インスタンスを1度だけ生成して使い回す(再利用する)必要があります。
スレッドセーフではないという制約
System.Random クラスのインスタンスメソッドはスレッドセーフではありません。
マルチスレッド環境(例えば Parallel.For や複数の Task 内)で単一の Random インスタンスにアクセスすると、内部状態が破壊され、常に「0」を返すようになったり、例外が発生したりするリスクがあります。
かつてはこの問題を解決するために、[ThreadStatic] 属性を用いたり、lock 文で排他制御を行ったりする複雑な実装が必要でした。
最新の推奨手法:Random.Shared の活用
現代の C# 開発において、最も推奨される乱数生成の方法は Random.Shared プロパティを使用することです。
これは .NET 6 で導入された仕組みで、それ以降のすべてのバージョンで標準的な手法となっています。
Random.Shared の利点
Random.Shared は、Random クラスの静的な共有インスタンスを提供します。
これには以下の大きなメリットがあります。
- スレッドセーフである:内部的にスレッドごとに適切な管理が行われているため、開発者が
lockを書く必要がありません。 - パフォーマンスが良い:インスタンス化のオーバーヘッドがなく、スレッド間の競合も最小限に抑えられています。
- コードが簡潔になる:
new Random()と書く手間が省けます。
実装例
// インスタンス化不要でどこからでも呼び出せる
int dice = Random.Shared.Next(1, 7);
double probability = Random.Shared.NextDouble();
Console.WriteLine($"サイコロの目: {dice}");
Console.WriteLine($"確率: {probability}");
マルチスレッド環境での利用例:
Parallel.For(0, 10, i =>
{
// 各スレッドから安全にアクセス可能
int val = Random.Shared.Next(100);
Console.WriteLine($"Thread {Task.CurrentId}: {val}");
});
特別な理由がない限り、現在および今後の C# 開発では Random.Shared を第一選択肢として考えるべきです。
セキュリティが重要な場合の乱数生成
ここまでは「疑似乱数」について解説してきましたが、疑似乱数はアルゴリズムに基づいているため、原理的には予測可能です。
パスワードの生成、暗号化キー、セッションIDの発行など、セキュリティが絡む用途に System.Random を使用してはいけません。
RandomNumberGenerator クラス
セキュリティ要件を満たすためには、System.Security.Cryptography 名前空間にある RandomNumberGenerator クラスを使用します。
これは「暗号論的な疑似乱数生成器(CSPRNG)」であり、予測が極めて困難です。
整数を取得する方法
かつてはバイト配列を作成して変換する手間が必要でしたが、現在の .NET では非常に便利な静的メソッドが用意されています。
using System.Security.Cryptography;
// 0以上100未満の安全な乱数を取得
int secureNum = RandomNumberGenerator.GetInt32(0, 100);
Console.WriteLine($"安全な乱数: {secureNum}");
ランダムな文字列(トークン)を生成する方法
APIキーなどの生成には、バイト配列を直接取得する方法が有効です。
using System.Security.Cryptography;
byte[] data = new byte[32];
RandomNumberGenerator.Fill(data); // 強力な乱数で埋める
string token = Convert.ToBase64String(data);
Console.WriteLine($"生成されたトークン: {token}");
| 特徴 | System.Random / Random.Shared | RandomNumberGenerator |
|---|---|---|
| 生成速度 | 非常に高速 | やや低速 |
| 予測可能性 | 予測可能(シード依存) | 予測不能(暗号学的) |
| 主な用途 | ゲーム、シミュレーション、UI演出 | パスワード、暗号化、認証 |
| スレッドセーフ | Shared はセーフ | セーフ |
用途を間違えると重大な脆弱性につながるため、必ず「予測されても良いか」という観点で選択してください。
配列やリストの操作に役立つ最新メソッド
.NET 8 以降、Random クラスにはコレクション操作を支援する強力なメソッドが追加されました。
これらは 2026年現在の開発においても、コードの可読性を高める重要な要素です。
Shuffle メソッド
配列やスパンの内容をランダムに並び替えるメソッドです。
従来は「フィッシャー・イェーツのシャッフル」などを自前で実装する必要がありましたが、現在は 1 行で記述できます。
string[] members = { "田中", "佐藤", "鈴木", "高橋", "伊藤" };
// 配列の中身をランダムに並び替え
Random.Shared.Shuffle(members);
Console.WriteLine("シャッフル後の順序:");
foreach (var name in members)
{
Console.WriteLine(name);
}
GetItems メソッド
指定した配列やリストから、ランダムに要素を抽出します。
重複を許容して複数の要素を取り出す場合に非常に便利です。
string[] colors = { "Red", "Blue", "Green", "Yellow" };
// colorsからランダムに10個の要素を抽出(重複あり)
string[] pickedColors = Random.Shared.GetItems(colors, 10);
Console.WriteLine("抽出された色: " + string.Join(", ", pickedColors));
乱数生成におけるパフォーマンスの最適化
大規模なシミュレーションや、高頻度で乱数を生成するリアルタイム処理では、乱数生成自体のパフォーマンスがボトルネックになることがあります。
Span との連携
最新の C# では、メモリ割り当てを抑えるために Span<T> が多用されます。
乱数生成においても、バイト配列をヒープに確保するのではなく、スタック上の領域に直接書き込む手法が有効です。
// スタック上に領域を確保(短いデータの場合)
Span<byte> buffer = stackalloc byte[16];
// Random.Shared で buffer を埋める
Random.Shared.NextBytes(buffer);
// RandomNumberGenerator で安全に埋める
RandomNumberGenerator.Fill(buffer);
このように stackalloc と組み合わせることで、GC(ガベージコレクション)への負荷を最小限に抑えつつ、高速な乱数生成が可能になります。
非決定的なテストの回避(シード値の固定)
パフォーマンスとは直接関係ありませんが、バグの再現性を確保するために「あえて予測可能な乱数」が必要な場合があります。
その際は Random.Shared ではなく、特定のシード値を指定してインスタンスを生成します。
// シード値を固定することで、常に同じ順番で数値が生成される
int seed = 12345;
Random fixedRand = new Random(seed);
for (int i = 0; i < 3; i++)
{
Console.WriteLine(fixedRand.Next(1, 100));
}
これにより、単体テストなどで「特定の乱数パターンが発生したときの挙動」を確実に検証できるようになります。
まとめ
C#における乱数生成は、ここ数年で大幅に利便性と安全性が向上しました。
最後に、適切な使い分けを整理します。
- 一般的な用途(ゲーム、シミュレーション、UIなど)
Random.Sharedを使用してください。スレッドセーフかつ高速であり、最新の .NET 環境において最も標準的な選択肢です。 - セキュリティ用途(パスワード、トークン、認証など)
RandomNumberGenerator.GetInt32やRandomNumberGenerator.Fillを使用してください。System.Random を流用することは厳禁です。 - 配列の操作(シャッフル、サンプリング)
.NET 8 で追加されたShuffleやGetItemsを活用し、自前実装を減らして可読性を高めましょう。 - 再現性が必要なテスト
シード値を指定してnew Random(seed)を使用します。
これらの手法を正しく使い分けることで、バグが少なく、安全でパフォーマンスの高い C# アプリケーションを構築することができます。
2026年以降のモダンな開発現場では、「なぜその乱数生成手法を選んだのか」を明確に説明できることが、プロフェッショナルなエンジニアに求められるスキルの一つとなります。
