LoginSignup
7
4

More than 5 years have passed since last update.

別々のSystem.Randomインスタンスが同じ乱数列を生成してしまう

Last updated at Posted at 2018-01-17

注意: C#初心者なので、とんちんかんなことをいっているかもしれません。呼び出すたびに5桁のランダムな整数からなるパスワードを生成するCreatePasswordというメソッドを作成。5回連続でこのメソッドを呼び出したところ、5回とも同じパスワードが生成されるという事象が発生しました(´・ω・`)

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(CreatePassword()); // => 83740
        Console.WriteLine(CreatePassword()); // => 83740
        Console.WriteLine(CreatePassword()); // => 83740
        Console.WriteLine(CreatePassword()); // => 83740
        Console.WriteLine(CreatePassword()); // => 83740
    }

    static string CreatePassword()
    {
        var random = new Random();
        List<int> digits = new List<int>();
        while (digits.Count < 5)
        {
            digits.Add(random.Next(10));
        }
        return string.Join("", digits);
    }
}

そこでインターネットの叡智にすがってみました。そもそもSystem.RandomのコンストラクタはRandom()Random(int32)の2種類があります。後者は引数としてシード値を与えているのですが、引数を省略した場合(つまり前者の場合)、シード値はPCが起動してからの時間を保持するSystem.Environment.TickCountが使用されます。要するにnew Random()は内部的にはnew Random(System.Environment.TickCount)と同じということになります。

問題はこのTickCountプロパティがミリ秒単位であるということ。つまり1ミリ秒以内に複数のRandomインスタンスがコンストラクタRandom()により生成された場合、すべて同じシード値になり、結果として同じ乱数列が生成されてしまいます。

そこで以下のようにsleepを利用して、Random()のシード値であるTickCountの値を無理やり変えてやると、sleepの前と後ろで乱数列が異なる、つまりシード値が変化したような挙動を見せています。前3つと後3つはそれぞれ1ミリ秒以内に生成され、同じシード値を共有しているのでしょう。(コンパイラの最適化なども「悪さ」をしているような気がしますが、そこまでは追いきれませんでした……)

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(CreatePassword()); // => 38067
        Console.WriteLine(CreatePassword()); // => 38067
        Console.WriteLine(CreatePassword()); // => 38067

        System.Threading.Thread.Sleep(1000); // sleep in 1 sec.

        Console.WriteLine(CreatePassword()); // => 72012
        Console.WriteLine(CreatePassword()); // => 72012
        Console.WriteLine(CreatePassword()); // => 72012
    }

    static string CreatePassword()
    {
        var random = new Random();
        List<int> digits = new List<int>();
        while (digits.Count < 5)
        {
            digits.Add(random.Next(10));
        }
        return string.Join("", digits);
    }
}

原因が分かったとして、では冒頭のコードを想定通り動作させるにはどう修正すればよいかですが、不用意にSystem.Randomインスタンスを生成せずに使いまわすというのが手っ取り早そうです。繰り返すようにRandom()の乱数らしさは「時間」に依存していますが、この「時間」をプログラム上でコントロールするのは容易ではない。だとすれば「時間」の流れによって変化しないロジック、すなわち初期化された状態のままのインスタンスを使いまわすロジックを採用するのは、そう悪くない戦略だと思います。

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(CreatePassword()); // => 60926
        Console.WriteLine(CreatePassword()); // => 54621
        Console.WriteLine(CreatePassword()); // => 62504
        Console.WriteLine(CreatePassword()); // => 67956
        Console.WriteLine(CreatePassword()); // => 91760
    }

    static Random random = new Random();

    static string CreatePassword()
    {
        List<int> digits = new List<int>();
        while (digits.Count < 5)
        {
            digits.Add(random.Next(10));
        }
        return string.Join("", digits);
    }
}

確かに修正後はCreatePasswordをコールするたびに、ランダムなパスワードが生成されていますね(`・ω・´)シャキーン

参考:

7
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4