from:https://www.xcode.me/more/net-csharp-generate-random
隨機數生成方法可以說是任何編程語言必備的功能,它的重要性不言而言,在C#中我們通常使用Random類生成隨機數,在一些場景下,我卻發現Random生成的隨機數並不可靠,在下面的例子中我們通過循環隨機生成5個隨機數:
for (int i = 0; i < 5; i++) { Random random = new Random(); Console.WriteLine(random.Next()); }
這段代碼執行后的結果如下所示:
2140400647 2140400647 2140400647 2140400647 2140400647
通過以上結果可知,隨機數類生成了5個相同的數,這並非我們的預期,為什么呢?為了弄清楚這個問題,零度剖析了微軟官方的開源Random類,發現在C#中生成隨機數使用的算法是線性同余法,經百科而知,這種算法生成的不是絕對隨機,而是一種偽隨機數,線性同余法算法的的公式是:
第N+1個數 = ( 第N個數 * A + B) % M
上面的公式中A、B和M分別為常數,是生成隨機數的因子,如果之前從未通過同一個Random對象生成過隨機數(也就是調用過Next方法),那么第N個隨機數為將被指定為一個默認的常數,這個常數在創建一個Random類時被默認值指定,Random也提供一個構造函數允許開發者使用自己的隨機數因子,這一切可通過微軟官方開源代碼看到:
public Random() : this(Environment.TickCount) { } public Random(int Seed) { }
通過默認構造函數創建Random類時,一個Environment.TickCount對象作為因子被默認傳遞給第二個構造函數,Environment.TickCount表示操作系統啟動后經過的毫秒數,計算機的運算運算速度遠比毫秒要快得多,這導致一個的具有毫秒精度的因子參與隨機數的生成過程,但在5次循環中,我們使用了同一個毫秒級的因子,從而生成相同的隨機數,另外,第N+1個數的生成與第N個數有着直接的關系。
在上面的例子中,假設系統啟動以來的毫秒數為888毫秒,執行5次循環用時只有0.1毫秒,這導致在循環中創建的5個Random對象都使用了相同的888因子,每次被創建的隨機對象又使用了相同的第N個數(默認為常數),通過這樣的假設我們不難看出,上面的結果是必然的。
現在我們改變這個格局,在循環之外創建一個Random對象,在每次循環中引用它,並通過它生成隨機數,並在同一個對象上多次調用Next方法,從而不斷變化第N個數,代碼如下所示:
Random random = new Random(); for (int i = 0; i < 5; i++) { Console.WriteLine(random.Next()); }
執行后的結果如下所示:
391098894 1791722821 1488616582 1970032058 201874423
我們看到這個結果確實證實了我們上面的推斷,第1次循環時公式中的第N個數為默認常數;當第二次循環時,第N個數為391098894,隨后不斷變化的第N個數作為因子參與計算,這保證了結果的隨機性。
雖然通過我們的隨機數看起來也很隨機了,但必定這個算法是偽隨機數,當第N個數和因子都相同時,生成的隨機數仍然是重復的隨機數,由於Random提供一個帶參的構造函數允許我們傳入一個因子,如果傳入的因子隨機性強的話,那么生成的隨機數也會比較可靠,為了提供一個可靠點的因子,我們通常使用GUID產生填充因子,同樣放在循環中測試:
for (int i = 0; i < 5; i++) { byte[] buffer = Guid.NewGuid().ToByteArray(); int iSeed = BitConverter.ToInt32(buffer, 0); Random random = new Random(iSeed); Console.WriteLine(random.Next()); }
這樣的方式保證了填充因子的隨機性,所以生成的隨機數也比較可靠,運行結果如下所示:
734397360 1712793171 1984332878 819811856 1015979983
在一些場景下這樣的隨機數並不可靠,為了生成更加可靠的隨機數,微軟在System.Security.Cryptography命名空間下提供一個名為RNGCryptoServiceProvider的類,它采用系統當前的硬件信息、進程信息、線程信息、系統啟動時間和當前精確時間作為填充因子,通過更好的算法生成高質量的隨機數,它的使用方法如下所示:
byte[] randomBytes = new byte[4]; RNGCryptoServiceProvider rngServiceProvider = new RNGCryptoServiceProvider(); rngServiceProvider.GetBytes(randomBytes); Int32 result = BitConverter.ToInt32(randomBytes, 0);
通過這種算法生成的隨機數,經過成千上萬次的測試,並未發現重復,質量的確比Random高了很多。另外windows api也提供了一個非托管的隨機數生成函數CryptGenRandom,CryptGenRandom與RNGCryptoServiceProvider的原理類似,采用C++編寫,如果要在.NET中使用,需要進行簡單的封裝。它的原型如下所示:
BOOL WINAPI CryptGenRandom( _In_ HCRYPTPROV hProv, _In_ DWORD dwLen, _Inout_ BYTE *pbBuffer );
以上就是零度為您帶來的隨機數生成方法和基本原理,您可以通過需求和場景選擇最佳的方式,Random算法簡單,性能較高,適用於隨機性要求不高的情況,由於RNGCryptoServiceProvider在生成期間需要查詢上面提到的幾種系統因子,所以性能稍弱於Random類,但隨機數質量高,可靠性更好。
