C#で乱数を作成する & 毎回異なるシードを指定する方法

C#(Unity以外/.NET FM, .NET Core)で乱数を生成するには、System.Randum クラスを使用します。

Randomクラスの基本的な使い方

使用方法は以下の通り。

public static void Foo()
{
    var rand = Random();
    int value = rand.Next(); // 0~2147483647までの乱数が取れる
    
    Console.WriteLine(value);
    // > 1949677498
    
    // 最大値の指定
    int value_2 = rand(3); // 0~2までの乱数が取れる(★★★3は含まない!!)
    Console.WriteLine(value_2);
    // > 0 or 1 or 2
    
    // 範囲の指定
    int value_3 = rand(1, 3); // 1~2までの乱数が取れる(★★★3は含まない!!)
    Console.WriteLine(value_2);
    // > 1 or 2
}

コメントにも書きましたが範囲指定した場合、最大値に指定した値は乱数に含まれません。

最大値-1までの値が乱数として取得できます。またint型の正の数だけが取り扱われます。

因みにUnityの乱数生成は.NETとは異なる、なり、UnityEngine.Rondomクラスがあり使用方法が微妙に異なりますがここでは取り扱いません。

短時間でRandomクラスを複数インスタンス化すると乱数が同値になる

しかし短時間で複数のRandomクラスのインスタンスを作成して乱数を生成すると同じ値になってしまいます。

そのような場合、複数のインスタンスを作成するのではなく同じインスタンスを使用してNextメソッドを呼び出せば回避できますが、事情でそうできない場合もあります。

例えば以下のようなコードを書いてしまうと異なるインスタンス間でNextの結果が同じになることがあります。

public static void TickToRandum()
{
    var list = new List<Random>();
    for(int i = 0; i < 100; i++)
    {
        list.Add(new Random());
    }

    foreach (var r in list)
    {
        Console.WriteLine(r.Next()); // インスタンスが違うけど全部同じ値になってしまう
        // > 1949677498
        // > 1949677498
        // ....
    }
}

乱数の生成には、大抵の実装で「シード値」という値が必要です。Randomクラスも同様です。引数なしのコンストラクタを呼び出した場合、内部で以下のようにシード値を指定しています。

public Random() : this(Environment.TickCount) { } // TickCountが内部で指定される
public Random(int Seed) { ... }

この「Environment.TickCount」はPCが起動してからのミリ秒単位の時間を記録しているint型の変数ですが、これが冒頭に記載したコードでミリ秒以下で参照されています。結果として同じミリ秒のシード値が使用され、Nextで得られる乱数が同値となります。(ミリ秒はCPU時間からするとかなり長い時間です)

この現象を避ける方法ですが、以下2つが考えられます。

  1. Randomクラスのインスタンスを全体で使いまわす
  2. ミリ秒以下の呼び出しでも異なるシード値を指定する

回避案(1) Randomクラスのインスタンスを全体で使いまわす

(1) ですがすごく簡単で、以下のように同じRandomクラスを使いまわすMyRandomクラスを作成してシステム全体から使用する方法です。

// インスタンスを使いまわすランダムの実装
public static class MyRandom
{
    private static Random random;

    public static int Next()
    {
        if (random == null) random = new Random();
        return random.Next();
    }

    public static int Next(int maxValue)
    {
        if (random == null) random = new Random();
        return random.Next(maxValue);
    }

    public static int Next(int minValue, int maxValue)
    {
        if (random == null) random = new Random();
        return random.Next(minValue, maxValue);
    }
}

回避案(2) ミリ秒以下の呼び出しでも異なるシード値を指定する

(2) は少し考える必要があります。GUIDをシードにして整数の乱数を作る という方法が紹介されていますが、シード値が int型なので128ビットの上位32bitしか使用していない、かつ、GUIDの生成という割と重めのコスト支払いが生かせていません。

生成が高速で行われる場合、PC上の時刻をシード値として使用することは実質不可能なので、(1) と類似の方法でオブジェクトを生成する処理を作成します。コードは以下の通り。

// ユニークなSEED値を持つRandomオブジェクトを生成するためのクラス
public static class MyRandom
{
    // 乱数のSeed値に乱数を使用する
    private static Random random = new Random();
    public static Random Create() => new Random(random.Next());
}

これを以下のように使用すると短時間に複数のRandomオブジェクトを作成してもNextで得られる乱数の値が異なるオブジェクトを取得できます。

// 使い方
var list = new List<Random>();
for(int i = 0; i < 100; i++)
{
    list.Add(MyRandom.Create()); // ★ここを変更した
}

foreach (var r in list)
{
    Console.WriteLine(r.Next()); // 必ず違う値になる
    // > 314216147
    // > 1401494015
    // > 370983633
    // ....
}

上記あくまで一例で、これ以外に簡単な実装方法もあると思います。重要なのは、Randomクラスの生成をユーザーコード内で直接扱わない。何らかのラッパーをかませてシード値をRandomクラスに指定する事かと思います。

以上です。