C# Random Seedとは?乱数が同じになる原因と再現性のある使い方を徹底解説
はじめに
C#で乱数を扱うときによく使われるのがSystem.Randomクラスです。ゲームの抽選、テストデータの作成、シミュレーション、ランダムな並び替えなど、さまざまな場面で利用されます。
しかし、Randomを使っていると「なぜか毎回同じ乱数になる」「ループ内で同じ値ばかり出る」「seedを指定したら結果が固定された」といった問題に遭遇することがあります。
このとき重要になるのが、Random Seed、つまり乱数生成の初期値です。
C#のRandomは完全な意味でのランダムではなく、疑似乱数を生成します。疑似乱数は、内部状態とアルゴリズムに基づいて数値列を作る仕組みです。そのため、同じseedから開始すれば、同じ順番の乱数列を再現できます。
この記事では、C#におけるRandom Seedの意味、乱数が同じになる原因、正しいRandomの使い方、再現性が必要な場面でのseed活用方法、さらにRandomNumberGeneratorとの違いまで詳しく解説します。
1. C# Random Seedとは?乱数生成におけるseedの基本
C#のRandom Seedとは、乱数列を生成するための出発点となる値です。
Randomクラスは「乱数」と呼ばれる値を生成しますが、実際には数学的なアルゴリズムによって計算された疑似乱数です。疑似乱数は、初期状態が同じであれば同じ順番で値が生成されます。
その初期状態を決めるために使われる値がseedです。
1-1. seedは乱数列の初期値を決める値
seedは、乱数生成器が最初にどの状態から処理を始めるかを決める値です。
たとえば、次のようにRandomのコンストラクターに整数を渡すと、その値がseedとして使われます。
C#Random random = new Random(123);
この場合、123というseedをもとに乱数列が生成されます。
重要なのは、seedが乱数そのものではなく、乱数列を作るための初期値であるという点です。seedを変えると生成される乱数列も変わり、同じseedを使うと同じ乱数列が再現されます。
1-2. 同じseedを指定すると同じ乱数列になる理由
同じseedを指定すると同じ乱数列になるのは、System.Randomが疑似乱数生成器だからです。
疑似乱数生成器は、内部の状態を更新しながら次の値を計算します。最初の状態が同じで、同じ順番でメソッドを呼び出せば、計算結果も同じになります。
たとえば、次のコードを何度実行しても、同じ環境・同じ条件では同じ順番の値が出力されます。
C#Random random = new Random(100);
Console.WriteLine(random.Next(1, 10));
Console.WriteLine(random.Next(1, 10));
Console.WriteLine(random.Next(1, 10));
これは不具合ではありません。むしろ、seedを指定した場合の正しい動作です。
テストやシミュレーションでは、この性質を利用することで「同じ条件で同じ結果を再現する」ことができます。
1-3. seedを指定しない場合にC#内部で行われる処理
seedを指定せずにRandomを作成する場合は、次のように記述します。
C#Random random = new Random();
この場合、C#側で自動的に初期化が行われます。
古い.NETでは、短時間に複数のRandomインスタンスを作成すると、内部的に近いseedが使われ、同じような乱数列になる問題が起こりやすいケースがありました。そのため、以前から「new Random()をループ内で何度も呼ばない」という注意点がよく知られています。
現在の.NETでは実装が改善されていますが、それでもRandomインスタンスを必要以上に作り直す設計は避けるべきです。基本的には、1つのRandomインスタンスを使い回すか、.NET 6以降であればRandom.Sharedを使うのが自然です。
1-4. 「ランダムなのに再現できる」とはどういうことか
「乱数なのに再現できる」と聞くと矛盾しているように感じるかもしれません。
しかし、プログラムで扱う多くの乱数は、完全な自然現象としてのランダムではなく、疑似乱数です。疑似乱数は、外から見るとランダムに見える数値列を、アルゴリズムによって生成します。
つまり、次の2つは両立します。
ランダムに見える値を生成できる。
同じseedを使えば同じ結果を再現できる。
この性質は、開発では非常に便利です。
たとえば、ゲームのマップ生成でseedを固定すれば、同じマップを何度でも再生成できます。テストでseedを固定すれば、失敗したケースを再現しやすくなります。
一方で、パスワードや認証トークンのように予測されてはいけない値には、seedによって再現できるRandomは向いていません。
2. C#でRandom Seedを使う基本構文
C#でRandom Seedを使うには、System.Randomクラスのコンストラクターに整数値を渡します。
基本構文を理解しておくと、seedを固定すべき場面と固定すべきでない場面を使い分けやすくなります。
2-1. System.Randomクラスの基本的な使い方
System.Randomは、C#で一般的な疑似乱数を生成するためのクラスです。
基本的な使い方は次のとおりです。
C#Random random = new Random();
int value = random.Next();
Console.WriteLine(value);
Next()は、0以上のランダムな整数を返します。
範囲を指定したい場合は、次のように書きます。
C#Random random = new Random();
int dice = random.Next(1, 7);
Console.WriteLine(dice);
この例では、1以上7未満、つまり1から6までの整数が生成されます。サイコロのような処理を書くときによく使われる形です。
2-2. seedを指定してRandomインスタンスを作成する方法
seedを指定する場合は、Randomのコンストラクターに整数を渡します。
C#Random random = new Random(42);
この42がseedです。
同じseedを指定して同じ順番で乱数を取得すると、同じ乱数列が得られます。
C#Random random1 = new Random(42);
Random random2 = new Random(42);
Console.WriteLine(random1.Next(1, 100));
Console.WriteLine(random1.Next(1, 100));
Console.WriteLine(random2.Next(1, 100));
Console.WriteLine(random2.Next(1, 100));
random1とrandom2は同じseedから開始しているため、同じ順番でNextを呼び出せば同じ値が出力されます。
2-3. Next・NextDouble・NextBytesの使い分け
System.Randomには、主に次のようなメソッドがあります。
Next()は整数を生成します。
C#int value = random.Next();
Next(int maxValue)は、0以上maxValue未満の整数を生成します。
C#int value = random.Next(10);
この場合、0から9までの値が生成されます。
Next(int minValue, int maxValue)は、minValue以上maxValue未満の整数を生成します。
C#int value = random.Next(1, 101);
この場合、1から100までの値が生成されます。
NextDouble()は、0.0以上1.0未満のdouble値を生成します。
C#double rate = random.NextDouble();
確率判定などでよく使われます。
C#if (random.NextDouble() < 0.3)
{
Console.WriteLine("30%の確率で発生");
}
NextBytes()は、指定したバイト配列にランダムなバイト値を格納します。
C#byte[] buffer = new byte[8];
random.NextBytes(buffer);
ただし、NextBytes()で生成される値もSystem.Randomによる疑似乱数です。セキュリティ用途には使わないようにしましょう。
2-4. seedあり・seedなしの実行結果の違い
seedありの場合、同じseedを使う限り、同じ乱数列を再現できます。
C#Random random = new Random(10);
Console.WriteLine(random.Next(1, 100));
Console.WriteLine(random.Next(1, 100));
Console.WriteLine(random.Next(1, 100));
このコードは、同じ条件で実行すれば同じ順番の値を出力します。
一方、seedなしの場合は、通常は実行のたびに異なる乱数列になります。
C#Random random = new Random();
Console.WriteLine(random.Next(1, 100));
Console.WriteLine(random.Next(1, 100));
Console.WriteLine(random.Next(1, 100));
毎回違う結果がほしい通常の処理では、seedなしでRandomを作成するか、Random.Sharedを利用します。
ただし、seedなしだからといって、インスタンスを何度も作り直してよいわけではありません。Randomは使い回すのが基本です。
3. C#の乱数が毎回同じになる主な原因
C#で「乱数が毎回同じになる」と感じる原因の多くは、Randomの使い方にあります。
特に、seedを固定している場合や、短時間にRandomインスタンスを何度も生成している場合は注意が必要です。
3-1. 同じseedを固定値で指定している
最もわかりやすい原因は、同じseedを固定値で指定していることです。
C#Random random = new Random(123);
このようにseedを固定すると、実行するたびに同じ乱数列が生成されます。
たとえば、次のコードは毎回同じ結果になります。
C#Random random = new Random(123);
for (int i = 0; i < 5; i++)
{
Console.WriteLine(random.Next(1, 100));
}
これはC#の不具合ではなく、seed固定による仕様どおりの動作です。
テストやデバッグでは便利ですが、ユーザーに毎回違う結果を見せたい処理では固定seedを使うべきではありません。
3-2. 短時間にRandomインスタンスを何度も生成している
乱数が同じになる原因としてよくあるのが、短時間にRandomインスタンスを何度も生成するパターンです。
C#Random random = new Random();
int value = random.Next(1, 100);
このコード自体は問題ありません。
しかし、これを短時間に何度も繰り返すと、環境や.NETのバージョンによっては同じような値が続く原因になることがあります。
特に古い.NET Frameworkでは、seedなしのRandomが時刻に基づいて初期化される関係で、短時間に複数のインスタンスを作ると同じseedになりやすい問題がありました。
そのため、乱数を生成するたびにnew Random()するのではなく、作成したRandomインスタンスを使い回す設計にすることが重要です。
3-3. ループ内でnew Random()を繰り返している
C#のRandomで非常によくあるアンチパターンが、ループ内でnew Random()を繰り返す書き方です。
C#for (int i = 0; i < 10; i++)
{
Random random = new Random();
Console.WriteLine(random.Next(1, 100));
}
このコードは、一見すると毎回新しい乱数生成器を作っているのでランダム性が高そうに見えます。
しかし実際には、毎回新しいRandomを作ることで、初期化のタイミングが近くなり、同じ値や似たような値が出やすくなる原因になります。
正しくは、ループの外でRandomを作成し、ループ内では同じインスタンスを使い回します。
C#Random random = new Random();
for (int i = 0; i < 10; i++)
{
Console.WriteLine(random.Next(1, 100));
}
このように書くことで、Randomの内部状態が正しく進み、自然な乱数列を得られます。
3-4. テスト用コードのseed指定が本番コードに残っている
開発中に再現性を確保するため、次のようにseedを固定することがあります。
C#Random random = new Random(999);
これはテストやデバッグでは便利です。
しかし、このコードが本番環境に残っていると、ユーザーが何度実行しても同じ結果になります。
たとえば、抽選処理、ゲーム内報酬、ランダム表示、シャッフル処理などで固定seedが残っていると、意図しない規則性が生まれます。
テスト用のseed固定は、設定ファイルや環境変数で切り替えられるようにするのがおすすめです。
C#int? seed = null; // 本番ではnull、テストでは固定値を設定
Random random = seed.HasValue
? new Random(seed.Value)
: new Random();
このようにすれば、テスト時だけseedを固定し、本番では通常の乱数生成に切り替えられます。
3-5. Unityやゲーム開発でseed管理を誤っている
Unityやゲーム開発では、seed管理のミスによって「毎回同じマップになる」「敵の出現パターンが固定される」「ランダムイベントが同じ順番で発生する」といった問題が起こることがあります。
ゲーム開発では、seedを固定すること自体は悪いことではありません。
たとえば、ローグライクゲームや自動生成マップでは、seedを共有することで同じステージを再現できます。プレイヤーに「ワールドseed」を入力させる設計もよくあります。
問題は、固定すべきでない場面まで同じseedを使ってしまうことです。
ステージ生成は再現性のためにseedを固定する。
ガチャや報酬抽選は予測されないように別の仕組みを使う。
デバッグ用seedは本番ビルドで無効にする。
このように、用途ごとにseedの扱いを分けることが重要です。
4. 乱数が同じになる問題を防ぐ正しいRandomの使い方
C#で乱数が同じになる問題を防ぐには、Randomインスタンスの作り方と使い回し方を正しく理解する必要があります。
基本はシンプルです。
Randomは何度も作り直さず、必要な範囲で使い回します。
4-1. Randomインスタンスは使い回すのが基本
Randomは、内部状態を持つ疑似乱数生成器です。
Next()などのメソッドを呼ぶたびに内部状態が進み、次の乱数が生成されます。そのため、同じインスタンスを継続して使うことで、自然な乱数列を得られます。
C#Random random = new Random();
int a = random.Next(1, 100);
int b = random.Next(1, 100);
int c = random.Next(1, 100);
このように、1つのRandomインスタンスから複数の値を取得するのが基本です。
逆に、毎回new Random()してしまうと、内部状態が毎回リセットされます。
C#int a = new Random().Next(1, 100);
int b = new Random().Next(1, 100);
int c = new Random().Next(1, 100);
この書き方は避けましょう。
4-2. static readonly Randomを使う方法
アプリケーション内で共通して乱数を使いたい場合は、static readonly Randomとして定義する方法があります。
C#public class RandomService
{
private static readonly Random Random = new Random();
public int GetDice()
{
return Random.Next(1, 7);
}
}
このようにしておけば、Randomインスタンスを毎回作り直さずに済みます。
ただし、複数スレッドから同時にアクセスする場合は注意が必要です。Randomインスタンスを共有するときは、スレッドセーフ性を考慮する必要があります。
シングルスレッドのコンソールアプリや簡単な処理であれば、static readonly Randomはわかりやすい選択肢です。
4-3. .NET 6以降ならRandom.Sharedを使う
.NET 6以降では、共有インスタンスとしてRandom.Sharedが用意されています。
C#int value = Random.Shared.Next(1, 100);
Random.Sharedを使うと、自分でRandomインスタンスを管理しなくても、簡潔に乱数を生成できます。
たとえば、サイコロを振る処理は次のように書けます。
C#int dice = Random.Shared.Next(1, 7);
Console.WriteLine(dice);
少量の乱数を手軽に使いたい場合や、アプリケーション全体で共有の乱数生成器を使いたい場合に便利です。
ただし、再現性が必要な処理ではRandom.Sharedではなく、明示的にseedを指定したRandomインスタンスを使うべきです。
C#Random random = new Random(123);
Random.Sharedは便利ですが、seedを固定して同じ結果を再現する用途には向いていません。
4-4. マルチスレッド環境で注意すべきポイント
マルチスレッド環境では、Randomの扱いに注意が必要です。
複数スレッドから同じRandomインスタンスに同時アクセスすると、意図しない挙動や競合の原因になる可能性があります。
.NET 6以降で特別な理由がなければ、共有用途ではRandom.Sharedを使うのが簡単です。
C#Parallel.For(0, 10, i =>
{
int value = Random.Shared.Next(1, 100);
Console.WriteLine(value);
});
一方で、スレッドごとに再現性のある乱数列を作りたい場合は、スレッドごとに異なるseedを与えたRandomインスタンスを用意する方法もあります。
C#Parallel.For(0, 4, i =>
{
Random random = new Random(1000 + i);
int value = random.Next(1, 100);
Console.WriteLine($"Thread {i}: {value}");
});
重要なのは、共有すべきか、スレッドごとに分けるべきか、再現性が必要かを設計段階で決めておくことです。
4-5. seedを固定すべき場面と固定すべきでない場面
seedは便利ですが、固定すればよいというものではありません。
seedを固定すべき場面は、再現性が必要な処理です。
たとえば、次のような場面です。
テストで同じデータを生成したい。
シミュレーションを同じ条件で比較したい。
ゲームのマップ生成を再現したい。
不具合調査のために乱数の流れを再現したい。
一方、seedを固定すべきでない場面もあります。
ユーザーごとに異なる結果を出したい。
毎回違う抽選結果にしたい。
予測されると困る処理を行いたい。
セキュリティに関係する値を生成したい。
特に、パスワード、トークン、認証コード、セッションIDなどにはSystem.Randomを使うべきではありません。これらにはRandomNumberGeneratorを使います。
5. 再現性のある乱数を作るためのRandom Seed活用例
Random Seedの最大のメリットは、乱数を再現できることです。
通常、ランダムな処理は実行するたびに結果が変わるため、不具合調査や検証が難しくなることがあります。そこでseedを固定すると、同じ乱数列を何度でも再現できます。
5-1. テストで同じ結果を再現する
自動テストでは、毎回結果が変わる乱数は扱いにくいものです。
たとえば、ランダムに商品を選ぶ処理や、ランダムな入力データを生成する処理では、テスト実行のたびに結果が変わると、失敗原因を特定しにくくなります。
そこでseedを固定します。
C#Random random = new Random(12345);
int value = random.Next(1, 100);
同じseedを使えば、テスト実行のたびに同じ乱数列が使われます。
これにより、テスト結果が安定し、失敗した場合も原因を追いやすくなります。
5-2. シミュレーション結果を検証しやすくする
シミュレーションでは、乱数を使ってさまざまな条件を再現することがあります。
たとえば、交通量のシミュレーション、在庫変動のシミュレーション、ゲームバランスの検証などです。
このときseedを固定しておくと、同じ条件で何度でもシミュレーションを実行できます。
C#int seed = 20240601;
Random random = new Random(seed);
for (int i = 0; i < 10; i++)
{
double probability = random.NextDouble();
Console.WriteLine(probability);
}
同じseedを使えば、アルゴリズムを変更したときに、変更前後の結果を比較しやすくなります。
「乱数の違いによる差」ではなく「ロジック変更による差」を確認できるため、検証の精度が上がります。
5-3. ゲームのマップ生成やステージ生成に使う
ゲーム開発では、seedを使ったマップ生成がよく利用されます。
たとえば、次のような処理です。
C#int seed = 777;
Random random = new Random(seed);
for (int y = 0; y < 5; y++)
{
for (int x = 0; x < 5; x++)
{
int tile = random.Next(0, 3);
Console.Write(tile);
}
Console.WriteLine();
}
このコードでは、seedが同じであれば同じタイル配置が生成されます。
これにより、プレイヤーがseedを共有して同じマップを遊べるようにしたり、開発者が問題のあるマップを再現したりできます。
ただし、ゲーム内のすべての乱数を同じseedで管理すると、予測可能になりすぎる場合があります。マップ生成用、演出用、抽選用など、用途ごとに乱数の扱いを分けることが大切です。
5-4. デバッグ時に乱数の挙動を追跡する
ランダム要素がある処理は、デバッグが難しくなりがちです。
「あるときだけエラーが出る」「特定のランダム条件でだけバグが発生する」といった場合、毎回違う乱数が使われると再現が困難です。
そこで、デバッグ時にはseedを固定します。
C#Random random = new Random(5000);
これにより、毎回同じ流れで処理を追跡できます。
また、バグが発生したときのseedを保存しておけば、後から同じ状況を再現できます。
5-5. seed値をログに残して不具合を再現する
再現性を活かすうえで非常に有効なのが、seed値をログに残す方法です。
C#int seed = Environment.TickCount;
Console.WriteLine($"Seed: {seed}");
Random random = new Random(seed);
このように、実行時に使ったseedをログに出力しておけば、不具合が発生した場合に同じseedを指定して再実行できます。
C#Random random = new Random(記録されたseed);
ランダム要素を含む処理では、seedをログに残すだけでデバッグ効率が大きく向上します。
特に、ゲーム、シミュレーション、ランダムテスト、データ生成処理ではおすすめの方法です。
6. C# Random Seedの実践コード例
ここでは、C#のRandom Seedを使った実践的なコード例を紹介します。
seedを固定する例、seedを変える例、悪い例、改善例、Random.Sharedを使う例を順番に確認しましょう。
6-1. seedを固定して同じ乱数列を生成するコード
次のコードは、seedを固定して同じ乱数列を生成する例です。
C#using System;
class Program
{
static void Main()
{
Random random = new Random(123);
for (int i = 0; i < 5; i++)
{
Console.WriteLine(random.Next(1, 100));
}
}
}
このコードは、同じ環境で同じ順番に実行すれば、毎回同じ乱数列を出力します。
再現性が必要なテストやデバッグでは、このようにseedを固定します。
6-2. seedを変えて異なる乱数列を生成するコード
seedを変えると、生成される乱数列も変わります。
C#using System;
class Program
{
static void Main()
{
Random random1 = new Random(100);
Random random2 = new Random(200);
Console.WriteLine("seed = 100");
for (int i = 0; i < 5; i++)
{
Console.WriteLine(random1.Next(1, 100));
}
Console.WriteLine("seed = 200");
for (int i = 0; i < 5; i++)
{
Console.WriteLine(random2.Next(1, 100));
}
}
}
random1とrandom2ではseedが異なるため、生成される乱数列も異なります。
テストケースを増やしたい場合は、seedを変えながら複数回実行する方法が有効です。
6-3. ループ内でRandomをnewしてはいけない例
次のコードは避けるべき例です。
C#using System;
class Program
{
static void Main()
{
for (int i = 0; i < 10; i++)
{
Random random = new Random();
Console.WriteLine(random.Next(1, 100));
}
}
}
このコードでは、ループのたびにRandomインスタンスを作成しています。
古い実行環境では、短時間に生成されたRandomが同じような初期状態になり、同じ値が繰り返される原因になることがあります。
また、設計としても無駄が多く、Randomの内部状態を活かせません。
6-4. Randomインスタンスを再利用する改善例
改善後のコードは次のとおりです。
C#using System;
class Program
{
static void Main()
{
Random random = new Random();
for (int i = 0; i < 10; i++)
{
Console.WriteLine(random.Next(1, 100));
}
}
}
Randomインスタンスをループの外で1回だけ作成し、ループ内では同じインスタンスを使い回します。
この書き方が基本です。
クラス内で使う場合は、フィールドとして保持してもよいでしょう。
C#public class Dice
{
private readonly Random _random = new Random();
public int Roll()
{
return _random.Next(1, 7);
}
}
このようにすれば、Roll()を呼ぶたびに同じRandomインスタンスから次の乱数を取得できます。
6-5. Random.Sharedを使ったシンプルな実装例
.NET 6以降では、Random.Sharedを使うことでさらに簡潔に書けます。
C#using System;
class Program
{
static void Main()
{
for (int i = 0; i < 10; i++)
{
int value = Random.Shared.Next(1, 100);
Console.WriteLine(value);
}
}
}
Random.Sharedを使えば、Randomインスタンスを自分で保持する必要がありません。
簡単な乱数処理では便利です。
ただし、次のように再現性が必要な場合は、明示的にseedを指定したRandomを使います。
C#Random random = new Random(123);
用途に応じて、Random.Sharedとseed付きRandomを使い分けましょう。
7. RandomとRandomNumberGeneratorの違い
C#には、乱数を生成する仕組みとしてSystem.RandomとSystem.Security.Cryptography.RandomNumberGeneratorがあります。
どちらも乱数を生成できますが、目的が大きく異なります。
7-1. System.Randomは一般的な疑似乱数生成向け
System.Randomは、一般的な疑似乱数生成に向いています。
たとえば、次のような用途です。
ゲーム内の演出。
テストデータの作成。
シミュレーション。
ランダムな並び替え。
簡単な抽選処理。
System.Randomの特徴は、seedを指定することで結果を再現できることです。
C#Random random = new Random(123);
このようにseedを指定すれば、同じ乱数列を再現できます。
そのため、再現性が重要なテストやデバッグに向いています。
7-2. RandomNumberGeneratorは暗号学的に安全な乱数向け
RandomNumberGeneratorは、暗号学的に強い乱数を生成するためのクラスです。
名前空間は次のとおりです。
C#using System.Security.Cryptography;
たとえば、ランダムな整数を生成する場合は次のように書けます。
C#int value = RandomNumberGenerator.GetInt32(1, 100);
バイト列を生成する場合は、次のように書けます。
C#byte[] bytes = RandomNumberGenerator.GetBytes(32);
RandomNumberGeneratorは、パスワード、トークン、認証コード、暗号鍵、ソルトなど、予測されてはいけない値を生成するために使います。
7-3. パスワード・トークン・認証コードにRandomを使ってはいけない理由
System.Randomは、セキュリティ用途には適していません。
理由は、疑似乱数生成器であり、seedや内部状態が推測されると、生成される値を予測される可能性があるためです。
たとえば、次のようなコードで認証コードを作るのは避けるべきです。
C#Random random = new Random();
int code = random.Next(100000, 1000000);
一見すると6桁のランダムなコードに見えますが、セキュリティ上重要な値としては不十分です。
認証コードを生成するなら、次のようにRandomNumberGeneratorを使います。
C#int code = RandomNumberGenerator.GetInt32(100000, 1000000);
Console.WriteLine(code);
セキュリティに関わる乱数では、「ランダムに見える」だけでは不十分です。攻撃者に予測されにくいことが重要です。
7-4. 用途別にどちらを使うべきか
通常のアプリケーション処理で、再現性や手軽さが重要な場合はSystem.Randomを使います。
C#Random random = new Random();
int value = random.Next(1, 100);
.NET 6以降で簡単に使いたい場合は、Random.Sharedも便利です。
C#int value = Random.Shared.Next(1, 100);
一方、セキュリティが関係する場合はRandomNumberGeneratorを使います。
C#int value = RandomNumberGenerator.GetInt32(1, 100);
用途別にまとめると、ゲームの演出、テストデータ、シミュレーション、再現可能なランダム処理にはRandomが向いています。
パスワード、トークン、認証コード、暗号鍵、ソルトにはRandomNumberGeneratorを使うべきです。
7-5. seedによる再現性が必要な場合の選び方
seedによる再現性が必要な場合は、基本的にSystem.Randomを選びます。
C#Random random = new Random(123);
RandomNumberGeneratorは予測困難な乱数を生成するためのものであり、seedを指定して同じ乱数列を再現する用途には向いていません。
つまり、選び方の基準は次のとおりです。
再現性が必要ならSystem.Random。
予測困難性が必要ならRandomNumberGenerator。
この違いを理解しておけば、C#の乱数処理で大きな設計ミスを避けられます。
8. C# Random Seedでよくある疑問
ここでは、C#のRandom Seedについてよくある疑問を解説します。
seedの値の決め方、現在時刻を使うべきか、毎回違う乱数を出す方法、同じseedなのに結果が違うケースなどを確認しましょう。
8-1. seedにはどんな値を指定すればよいか
Randomのseedには整数値を指定します。
C#Random random = new Random(123);
テストやデバッグであれば、わかりやすい固定値で問題ありません。
C#Random random = new Random(1);
Random random = new Random(42);
Random random = new Random(12345);
重要なのは、seedの値そのものに特別な意味があるのではなく、同じseedを使えば同じ乱数列を再現できるという点です。
複数のテストケースを作りたい場合は、seedを変えて実行します。
C#for (int seed = 1; seed <= 5; seed++)
{
Random random = new Random(seed);
Console.WriteLine(random.Next(1, 100));
}
このようにseedを管理すれば、ランダム性を使いながら再現性も確保できます。
8-2. seedを現在時刻にすれば安全なのか
seedに現在時刻を使う方法はよく見かけます。
C#int seed = Environment.TickCount;
Random random = new Random(seed);
この方法は、毎回違う乱数列を得たいだけなら使える場合があります。
しかし、「安全」という意味では不十分です。
現在時刻はある程度推測できるため、セキュリティ用途には向いていません。パスワード、トークン、認証コードなどに現在時刻seedのRandomを使うのは避けるべきです。
セキュリティ用途では、必ずRandomNumberGeneratorを使います。
C#int value = RandomNumberGenerator.GetInt32(100000, 1000000);
現在時刻seedは、あくまで一般的な疑似乱数の初期化方法のひとつであり、暗号学的に安全な乱数を作る方法ではありません。
8-3. 毎回違う乱数を出したい場合はどうすればよいか
毎回違う乱数を出したい場合は、通常はseedを固定しないようにします。
C#Random random = new Random();
また、.NET 6以降であればRandom.Sharedを使えます。
C#int value = Random.Shared.Next(1, 100);
大切なのは、乱数を取得するたびにnew Random()しないことです。
悪い例は次のとおりです。
C#int value = new Random().Next(1, 100);
この書き方を何度も繰り返すのではなく、Randomインスタンスを使い回します。
C#Random random = new Random();
int a = random.Next(1, 100);
int b = random.Next(1, 100);
int c = random.Next(1, 100);
毎回違う結果がほしい場合でも、Randomを毎回作り直す必要はありません。
8-4. 同じseedなのに結果が違うことはあるのか
同じseedを使っても、結果が違うことはあります。
主な原因は、乱数メソッドを呼び出す順番や回数が違うことです。
C#Random random = new Random(123);
int a = random.Next(1, 100);
int b = random.Next(1, 100);
この場合と、次のように途中でNextDouble()を呼ぶ場合では、後続の結果が変わります。
C#Random random = new Random(123);
int a = random.Next(1, 100);
double x = random.NextDouble();
int b = random.Next(1, 100);
同じseedでも、乱数生成器の内部状態はメソッドを呼ぶたびに進みます。そのため、呼び出し順や呼び出し回数が変われば、後続の値も変わります。
また、.NETのバージョンや実装の違いによって、同じseedでも生成結果が完全に同じであることを長期的に保証できない場合があります。
アプリケーション内のテストやデバッグでは便利ですが、異なるランタイム間で完全一致を前提にする場合は注意しましょう。
8-5. Randomの結果は完全なランダムなのか
System.Randomの結果は、完全なランダムではありません。
Randomは疑似乱数生成器です。数学的なアルゴリズムにより、ランダムに見える数値列を生成しています。
一般的なアプリケーション用途では十分に使えますが、次のような用途には向いていません。
パスワード生成。
認証トークン生成。
セッションID生成。
暗号鍵生成。
セキュリティコード生成。
これらにはRandomNumberGeneratorを使う必要があります。
一方で、ゲームの演出、シャッフル、テストデータ、シミュレーションなどではSystem.Randomが便利です。
完全なランダムかどうかよりも、用途に合っているかどうかを基準に選びましょう。
9. C# Random Seedを使うときのベストプラクティス
C#でRandom Seedを正しく使うには、再現性、ランダム性、セキュリティの3つを分けて考えることが重要です。
ここでは、実務で意識したいベストプラクティスを紹介します。
9-1. 再現性が必要な処理ではseedを明示する
テスト、デバッグ、シミュレーション、マップ生成など、同じ結果を再現したい処理ではseedを明示しましょう。
C#int seed = 12345;
Random random = new Random(seed);
seedを明示すれば、後から同じ条件で再実行できます。
特に、ランダム要素を含む不具合調査では、seedが残っているかどうかで再現性が大きく変わります。
C#Console.WriteLine($"Seed: {seed}");
ログにseedを出力しておくと、問題が起きたときに同じ乱数列で検証できます。
9-2. 通常の乱数処理ではRandomインスタンスを使い回す
通常の乱数処理では、Randomインスタンスを何度も作り直さず、使い回すのが基本です。
C#Random random = new Random();
for (int i = 0; i < 10; i++)
{
Console.WriteLine(random.Next(1, 100));
}
クラスで使う場合は、フィールドとして保持する方法もあります。
C#public class Lottery
{
private readonly Random _random = new Random();
public int Draw()
{
return _random.Next(1, 100);
}
}
.NET 6以降で手軽に使うなら、Random.Sharedも選択肢です。
C#int value = Random.Shared.Next(1, 100);
ただし、再現性が必要な処理ではRandom.Sharedではなく、seed付きのRandomインスタンスを使いましょう。
9-3. セキュリティ用途ではRandomNumberGeneratorを使う
セキュリティ用途では、System.Randomを使わないことが重要です。
パスワード、トークン、認証コード、セッションID、暗号鍵、ソルトなどにはRandomNumberGeneratorを使います。
C#using System.Security.Cryptography;
int code = RandomNumberGenerator.GetInt32(100000, 1000000);
Console.WriteLine(code);
ランダムなバイト列が必要な場合は、次のように書けます。
C#byte[] token = RandomNumberGenerator.GetBytes(32);
string base64 = Convert.ToBase64String(token);
Console.WriteLine(base64);
System.Randomは便利ですが、予測されると困る値には使わないようにしましょう。
9-4. seed値を設定ファイルやログで管理する
seedを使う処理では、seed値をコードに直接埋め込むだけでなく、設定ファイルやログで管理すると便利です。
たとえば、テスト環境では固定seedを使い、本番環境ではseedを固定しない設計にできます。
C#int? seed = GetSeedFromConfig();
Random random = seed.HasValue
? new Random(seed.Value)
: new Random();
このようにしておけば、環境ごとに挙動を切り替えられます。
また、実行時に生成したseedをログに残す設計も有効です。
C#int seed = Environment.TickCount;
Console.WriteLine($"Random seed: {seed}");
Random random = new Random(seed);
不具合発生時にログからseedを確認できれば、同じ条件で再現できます。
9-5. 用途に応じてseed固定と非固定を切り替える
Random Seedの扱いで最も重要なのは、用途に応じて固定と非固定を切り替えることです。
再現性が必要な処理ではseedを固定します。
C#Random random = new Random(123);
毎回違う結果がほしい通常処理ではseedを固定しません。
C#Random random = new Random();
.NET 6以降なら、共有の乱数生成にはRandom.Sharedも使えます。
C#int value = Random.Shared.Next(1, 100);
セキュリティ用途ではRandomNumberGeneratorを使います。
C#int value = RandomNumberGenerator.GetInt32(1, 100);
このように、目的に応じて使い分けることで、C#の乱数処理を安全かつ扱いやすくできます。
まとめ
C#のRandom Seedとは、疑似乱数列の初期状態を決める値です。
System.Randomは疑似乱数生成器であり、同じseedを指定すると同じ乱数列を生成します。この性質により、テスト、デバッグ、シミュレーション、ゲームのマップ生成などで再現性を確保できます。
一方で、毎回違う結果が必要な処理で固定seedを使うと、乱数が毎回同じになる原因になります。また、ループ内でnew Random()を繰り返す書き方も避けるべきです。
基本的な使い方は、Randomインスタンスを1回作成して使い回すことです。
C#Random random = new Random();
int value = random.Next(1, 100);
.NET 6以降であれば、手軽な乱数生成にはRandom.Sharedも利用できます。
C#int value = Random.Shared.Next(1, 100);
再現性が必要な場合は、seedを明示します。
C#Random random = new Random(123);
ただし、パスワード、トークン、認証コード、暗号鍵など、セキュリティに関わる値にはSystem.Randomを使ってはいけません。そのような用途では、RandomNumberGeneratorを使います。
C#int code = RandomNumberGenerator.GetInt32(100000, 1000000);
C#で乱数を正しく扱うには、「再現性が必要なのか」「毎回違う結果が必要なのか」「予測されてはいけない値なのか」を分けて考えることが大切です。
Random Seedを正しく理解して使い分ければ、乱数が同じになる原因を防ぎながら、テストしやすく、保守しやすいC#コードを書けるようになります。

