"It's hard to create humor because of the unfair competition from the real world." - Peter's Almanac

The following class can be used to replace System.Random. It has the same basic interface, but internally it used the cryptographic random number generator:

Download link

(All pseudo-random number generators suck to some degree or another, but this one is better than System.Random while still being as easy to use.)

Option Explicit On
Option Strict On
Option Compare Binary

Imports System Imports System.Security.Cryptography

Public Class Random2 Const BufferSize As Integer = 1024 Private b(BufferSize - 1) As Byte Private ptr As Integer = -1 Private rnd As RandomNumberGenerator

Private Shared _Inst As Random2 = Nothing Public Shared ReadOnly Property Instance() As Random2 Get Static lock As New Object SyncLock lock If _Inst Is Nothing Then _Inst = New Random2 End If Return _Inst End SyncLock End Get End Property

Public Sub New() rnd = RandomNumberGenerator.Create End Sub

Private Sub FillBytes() rnd.GetBytes(b) ptr = 0 End Sub

Public Function [Next]() As Integer Return Me.Next(Int32.MaxValue) End Function

Public Function [Next](ByVal minValue As Integer, ByVal maxValue As Integer) As Integer If minValue > maxValue Then Throw New Exception("'minValue' cannot be greater than 'maxValue'") Return Me.Next(maxValue - minValue) + minValue End Function

Private Function _Next(ByVal maxValue As Byte) As Integer Dim myMax As Byte = Byte.MaxValue - (Byte.MaxValue Mod maxValue) Do If ptr < 0 OrElse ptr + 1 >= b.Length Then FillBytes() Dim n As Byte = b(ptr) ptr += 1 If n < myMax Then Return CInt(n Mod maxValue) End If Loop End Function

Private Function _Next(ByVal maxValue As UShort) As Integer Dim myMax As UShort = UShort.MaxValue - (UShort.MaxValue Mod maxValue) Do If ptr < 0 OrElse ptr + 2 >= b.Length Then FillBytes() Dim n As UShort = BitConverter.ToUInt16(b, ptr) ptr += 2 If n < myMax Then Return CInt(n Mod maxValue) End If Loop End Function

Private Function _Next(ByVal maxValue As UInteger) As Integer Dim myMax As UInteger = UInteger.MaxValue - (UInteger.MaxValue Mod maxValue) Do If ptr < 0 OrElse ptr + 4 >= b.Length Then FillBytes() Dim n As UInteger = BitConverter.ToUInt32(b, ptr) ptr += 4 If n < myMax Then Return CInt(n Mod maxValue) End If Loop End Function

Public Function [Next](ByVal maxValue As Integer) As Integer Static lock As New Object SyncLock lock If maxValue <= 0 Then Return 0

If maxValue <= Byte.MaxValue Then Return _Next(CByte(maxValue)) ElseIf maxValue <= UInt16.MaxValue Then Return _Next(CUShort(maxValue)) Else Return _Next(CUInt(maxValue)) End If End SyncLock End Function

Public Sub NextBytes(ByVal b() As Byte) rnd.GetBytes(b) End Sub

Public Function NextBytes(ByVal numOfBytes As Integer) As Byte() Dim x(numOfBytes - 1) As Byte rnd.GetBytes(x) Return x End Function

Public Function NextDouble() As Double Return Me.Next / Int32.MaxValue End Function End Class

It may seem a bit odd to have 3 overloaded versions of the _Next function. This was done for performance reasons. If the maximum value you will accept is less than what can fit in a byte, it's faster to use a single random byte instead of extracting an entire 32 bit integer.

Another bit of logic is the check to make sure the random number used isn't too large. The basic algorithm for using RandomNumberGenerator to get a random number up to a certain value is this:

Const maxValue As Integer = 6 
Dim rnd As Security.Cryptography.RandomNumberGenerator = Security.Cryptography.RandomNumberGenerator.Create
Dim b(0) As Byte
rnd.GetBytes(b)
Dim result As Integer = b(0) Mod maxValue

The problem is this: There are 42 groups of 6 digits between 0 and 251. The remaining numbers (252-255) translate to 0-3. That gives a slight bias in favor of those numbers (the numbers 0-3 have a 16.8% probability; the numbers 4-5 have a 16.4% probability).

As the MaxValue gets larger, this bias can be exaggerated. If the MaxValue is 100 (generating results 0-99), there are 2 full sets of 100 between 0-199. The remaining bytes (200-255) will only generate the results 0-55. The numbers 0-55 will have a 1.17% probability; the numbers 56-99 will have a 0.78% probability. (Statistical tests run over large quantities of data will verify this bias.)

Using a different random byte whenever the selected byte is greater than the last value that could produce an unbiased result will cause the probabilities to be re-balanced. Performance may be affected slightly depending on the size of the MaxValue and how many values need to be ignored. In testing the worst Byte case (129), it appears it's still no slower than using the larger 16-bit integer type.

Note that, just like the System.Random class, the MaxValue parameters to the Next methods are actually exclusive of the values you will get. So, if you call ...

Dim r as New Random2
Dim n as Integer = r.Next(6)

... you will get a number from 0 to 5.

Dim r as New Random2
Dim n as Integer = r.Next(1,7)

... will give you a number from 1 to 6.

There are 3 interface differences between this class and System.Random:

1. This class has a shared Instance property that allows you to call it without instantiating it directly. This is a much more convenient way to invoke the methods of this function. Since the main Next method uses a shared buffer of random bytes, it has been coded with a SyncLock to make it thread-safe in case you choose to use the same instance in multiple threads.

2. This class's constructor doesn't support a static seed value. System.Security.Cryptography.RandomNumberGenerator doesn't have a way to emulate this behavior, so I didn't create a constructor for it.

3. A second overload of the GetBytes method exists that allows you to get the random bytes returned as the result of the function call instead of using a previously instantiated byte array:

Dim x() As Byte = r2.NextBytes(32)

ScrewTurn Wiki version 2.0.33. Current Page Count: 23. Some of the icons created by FamFamFam.