C#を用いたアプリケーション開発において、乱数の生成は非常に頻繁に利用される機能の一つです。
ゲーム開発におけるダメージ計算やアイテムのドロップ判定、セキュリティ分野でのトークン生成、さらにはシミュレーション統計など、その用途は多岐にわたります。
しかし、C#には複数の乱数生成手法が存在し、用途に合わせて適切な方法を選択しなければ、思わぬバグや脆弱性の原因になることがあります。
本記事では、従来のSystem.Randomクラスから、最新の.NET環境で推奨される高速かつ便利な実装手法までを詳しく解説します。
C#における乱数生成の基本:System.Randomクラス
C#で最も一般的に利用されるのが、System.Randomクラスです。
このクラスは疑似乱数ジェネレータ(PRNG)と呼ばれ、特定の数式に基づいて乱数の数列を生成します。
インスタンスの作成と基本的なメソッド
基本的な使い方は、Randomクラスのインスタンスを生成し、提供されているメソッドを呼び出すだけです。
using System;
// Randomクラスのインスタンスを作成
Random rand = new Random();
// 0以上の整数を取得
int num1 = rand.Next();
// 0以上100未満の整数を取得
int num2 = rand.Next(100);
// 10以上51未満(つまり10から50まで)の整数を取得
int num3 = rand.Next(10, 51);
// 0.0以上1.0未満の浮動小数点数を取得
double dbl = rand.NextDouble();
Console.WriteLine($"num1: {num1}");
Console.WriteLine($"num2: {num2}");
Console.WriteLine($"num3: {num3}");
Console.WriteLine($"dbl: {dbl}");
num1: 1572849302
num2: 42
num3: 15
dbl: 0.742918341
Nextメソッドは引数によって範囲を指定でき、「最小値は含むが、最大値は含まない」という仕様に注意が必要です。
例えば、rand.Next(1, 7)と記述した場合、サイコロのように1から6までの数値が返されます。
Randomクラスを利用する際の重要な注意点
初心者の方が陥りやすい罠として、インスタンス化のタイミングに関する問題があります。
シード値の重複問題
Randomクラスをループの中で毎回インスタンス化してはいけません。
// 悪い例:ループ内で毎回インスタンスを生成する
for (int i = 0; i < 5; i++)
{
Random r = new Random();
Console.WriteLine(r.Next(1, 101));
}
このコードを実行すると、多くの場合で全く同じ数値が連続して出力されることになります。
これは、Randomクラスのデフォルトのシード値が「現在のシステム時刻」に基づいているためです。
コンピュータの処理速度は非常に速いため、ミリ秒単位で見ると同じ時刻に複数のインスタンスが生成され、結果として同じ乱数列が開始されてしまいます。
この問題を避けるためには、単一のインスタンスを再利用するように設計する必要があります。
マルチスレッド環境での競合
System.Randomクラスはスレッドセーフではありません。
複数のスレッドから同時に同じRandomインスタンスのメソッドを呼び出すと、内部状態が破壊され、すべての結果が「0」を返し続けるような不具合が発生する可能性があります。
並列処理を行うアプリケーションでは、スレッドごとにインスタンスを分けるか、後述する最新のプロパティを使用してください。
.NET 6以降で推奨される「Random.Shared」プロパティ
.NET 6以降では、乱数生成をより安全かつ手軽に行うためのRandom.Sharedプロパティが導入されました。
現代のC#開発においては、特別な理由がない限り「Random.Shared」を使用するのがベストプラクティスです。
Random.Sharedの利点
- インスタンスを自分で管理(
new)する必要がない。 - スレッドセーフであることが保証されている。
- スレッドごとに最適化されたインスタンスが提供されるため、パフォーマンスが高い。
具体的な使い方は以下の通りです。
// インスタンス化不要で直接利用可能
int value = Random.Shared.Next(1, 101);
Console.WriteLine($"ランダムな数値: {value}");
このプロパティを使用することで、シード値の重複問題やスレッド競合の問題を一気に解決できます。
.NET 8で追加された便利な新機能
2026年現在、多くの現場で利用されている.NET 8以降では、さらに強力なメソッドが追加されています。
これらを使うことで、これまで自作していたアルゴリズムを標準機能に置き換えることができます。
Random.GetItemsメソッド(要素のランダム抽出)
配列やリストの中から、指定した個数の要素をランダムに取得できます。
string[] colors = { "Red", "Blue", "Green", "Yellow", "Purple" };
// 配列からランダムに3つの要素を取得(重複あり)
string[] picked = Random.Shared.GetItems(colors, 3);
Console.WriteLine(string.Join(", ", picked));
Random.Shuffleメソッド(配列のシャッフル)
配列の中身をランダムに並べ替える機能も標準搭載されました。
従来は「フィッシャー–イェーツのシャッフル」などのアルゴリズムを手動で実装する必要がありましたが、現在はメソッド一つで完結します。
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 配列の中身をランダムに並べ替える
Random.Shared.Shuffle(numbers);
Console.WriteLine(string.Join(", ", numbers));
Shuffleメソッドは、元の配列を直接書き換える(インプレース)点に注意してください。
暗号学的に安全な乱数生成(System.Security.Cryptography)
これまでに紹介したRandomクラスは「疑似乱数」であり、次の数値を予測することが理論上可能です。
そのため、パスワード生成、暗号鍵、認証トークンの発行などには使用してはいけません。
高い安全性が求められる場合は、RandomNumberGeneratorクラスを使用します。
RandomNumberGeneratorの使い方
.NET Core以降、このクラスも非常に簡潔に書けるようになりました。
using System.Security.Cryptography;
// 100未満の安全な乱数を生成
int secureNum = RandomNumberGenerator.GetInt32(0, 100);
// 安全なバイト配列(ソルトなどに利用)を生成
byte[] data = RandomNumberGenerator.GetBytes(32);
Console.WriteLine($"Secure Int: {secureNum}");
Console.WriteLine($"Secure Bytes: {Convert.ToBase64String(data)}");
RandomNumberGenerator.GetInt32は静的メソッドであるため、インスタンス管理の必要がなく、スレッドセーフです。
ただし、計算コストはRandom.Sharedよりも高いため、パフォーマンスが極限まで求められるゲームエンジンの演算などには不向きです。
用途別・乱数生成手法の比較表
どの手法を使うべきか迷った際は、以下の表を参考にしてください。
| 手法 | 適した用途 | スレッドセーフ | セキュリティ強度 |
|---|---|---|---|
Random.Shared | 一般的なUI演出、ゲームの計算、シミュレーション | ○(安全) | ×(予測可能) |
RandomNumberGenerator | パスワード、トークン、暗号化、セキュリティ関連 | ○(安全) | ◎(非常に高い) |
new Random(seed) | 再現性が必要なテストや特定のシミュレーション | ×(非推奨) | ×(予測可能) |
基本的には「通常の処理は Random.Shared」「セキュリティ用途は RandomNumberGenerator」と覚えておけば間違いありません。
実践:指定した長さのランダムな文字列を生成する
より実践的な例として、英数字を組み合わせたランダムなパスワード文字列を生成するコードを紹介します。
ここでは、安全性を考慮してRandomNumberGeneratorを使用します。
using System;
using System.Security.Cryptography;
using System.Linq;
public class PasswordGenerator
{
public static string Generate(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 指定した文字集合からランダムに要素を選択して配列化
// GetItemsは.NET 8以降で利用可能
char[] stringChars = RandomNumberGenerator.GetItems(chars.ToCharArray(), length);
return new string(stringChars);
}
}
// 実行例
string password = PasswordGenerator.Generate(12);
Console.WriteLine($"生成されたパスワード: {password}");
生成されたパスワード: k7B9wXz2Pq4m
RandomNumberGenerator.GetItemsを活用することで、ループ処理を書くことなく簡潔に実装できます。
この手法は、セッションIDの生成や一時的なファイル名の作成にも非常に有効です。
パフォーマンスの最適化:大量の乱数が必要な場合
1秒間に数百万回といった膨大な乱数生成を行う場合、呼び出しオーバーヘッドが無視できなくなることがあります。
そのようなケースでは、NextBytesメソッドを使用して、まとめて乱数データをバッファに取得するのが最も高速です。
byte[] buffer = new byte[8192];
Random.Shared.NextBytes(buffer);
// バッファ内のバイトデータを加工して利用
一度に大量のメモリ領域を乱数で埋めることで、メソッドの呼び出し回数を劇的に減らすことができ、実行速度が向上します。
ただし、通常のビジネスアプリケーションや一般的なゲーム制作において、ここまでの最適化が必要になるシーンは稀です。
まとめ
C#における乱数生成は、.NETの進化とともに非常に洗練されたものとなりました。
かつてのようにnew Random()のタイミングを気にする必要はなく、基本は「Random.Shared」を選択するというシンプルな指針で開発を進めることができます。
一方で、セキュリティが関わる場面では必ずRandomNumberGeneratorを使用するという区別を徹底してください。
.NET 8以降で追加されたShuffleやGetItemsなどの便利なメソッドを使いこなすことで、コードの可読性はさらに高まります。
適切な手法を選択し、バグのない堅牢で高速なアプリケーションを構築していきましょう。
