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)