可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
For testing purposes I'm creating random numbers with a given seed (i.e. not based on the current time).
Thus the whole program is deterministic.
If something happens, I'd like to be able to quickly restore a point "shortly before" the incident.
Therefore I need to be able to restore a System.Random
to a previous state.
Is there a way to extract a seed which I can use to recreate the random generator?
回答1:
In line with the answer given here, I wrote a small class to help with saving and restoring the state.
void Main()
{
var r = new Random();
Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("before save");
var s = r.Save();
Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("after save");
r = s.Restore();
Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("after restore");
s.Dump();
}
public static class RandomExtensions
{
public static RandomState Save(this Random random)
{
var binaryFormatter = new BinaryFormatter();
using (var temp = new MemoryStream())
{
binaryFormatter.Serialize(temp, random);
return new RandomState(temp.ToArray());
}
}
public static Random Restore(this RandomState state)
{
var binaryFormatter = new BinaryFormatter();
using (var temp = new MemoryStream(state.State))
{
return (Random)binaryFormatter.Deserialize(temp);
}
}
}
public struct RandomState
{
public readonly byte[] State;
public RandomState(byte[] state)
{
State = state;
}
}
You can test this code in LINQPad.
回答2:
This is what I came up:
Basically it extracts the private seed array.
You just need to be careful to restore an "unshared" array.
var first = new Random(100);
// gain access to private seed array of Random
var seedArrayInfo = typeof(Random).GetField("SeedArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var seedArray = seedArrayInfo.GetValue(first) as int[];
var other = new Random(200); // seed doesn't matter!
var seedArrayCopy = seedArray.ToArray(); // we need to copy since otherwise they share the array!
seedArrayInfo.SetValue(other, seedArrayCopy);
for (var i = 10; i < 1000; ++i)
{
var v1 = first.Next(i);
var v2 = other.Next(i);
Debug.Assert(v1 == v2);
}
回答3:
There is an alternative solution that (1) avoids the need to remember all previously generated numbers; (2) does not involve accessing the private fields of Random; (3) does not require serialization; (4) does not require looping back through Random as many times as it had been called; and (5) does not require creating a replacement for the built-in Random class.
The trick is to get state by generating a random number, and then reseeding the random number generator to this value. Then, in the future, one can always return to this state by reseeding the random number generator to this value. In other words, we "burn" a number in the random number sequence for the purpose of saving state and reseeding.
The implementation follows. Note that one would access the Generator property to actually generate numbers.
public class RestorableRandom
{
public Random Generator { get; private set; }
public RestorableRandom()
{
Generator = new Random();
}
public RestorableRandom(int seed)
{
Generator = new Random(seed);
}
public int GetState()
{
int state = Generator.Next();
Generator = new Random(state);
return state;
}
public void RestoreState(int state)
{
Generator = new Random(state);
}
}
And here is a simple test:
[Fact]
public void RestorableRandomWorks()
{
RestorableRandom r = new RestorableRandom();
double firstValueInSequence = r.Generator.NextDouble();
int state = r.GetState();
double secondValueInSequence = r.Generator.NextDouble();
double thirdValueInSequence = r.Generator.NextDouble();
r.RestoreState(state);
r.Generator.NextDouble().Should().Be(secondValueInSequence);
r.Generator.NextDouble().Should().Be(thirdValueInSequence);
}
回答4:
System.Random
is not sealed and its methods are virtual, so you could create a class that counts the number of numbers generated to keep track of the state, something like:
class StateRandom : System.Random
{
Int32 _numberOfInvokes;
public Int32 NumberOfInvokes { get { return _numberOfInvokes; } }
public StateRandom(int Seed, int forward = 0) : base(Seed)
{
for(int i = 0; i < forward; ++i)
Next(0);
}
public override Int32 Next(Int32 maxValue)
{
_numberOfInvokes += 1;
return base.Next(maxValue);
}
}
Example usage:
void Main()
{
var a = new StateRandom(123);
a.Next(100);
a.Next(100);
a.Next(100);
var state = a.NumberOfInvokes;
Console.WriteLine(a.Next(100));
Console.WriteLine(a.Next(100));
Console.WriteLine(a.Next(100));
// use 'state - 1' to be in the previous state instead
var b = new StateRandom(123, state);
Console.WriteLine(b.Next(100));
Console.WriteLine(b.Next(100));
Console.WriteLine(b.Next(100));
}
Output:
81
73
4
81
73
4
回答5:
I'm aware this question has already been answered, however, I wanted to provide my own implementation, which is currently in use for a game that I am creating. Essentially, I created my own Random class, using the code of .NET's Random.cs. Not only did I add more functionality, but I also added a way to save and load the current generator state into and from an array of just 59 indices. It is better to do it this way instead of how some other comments suggest to "Iterate x number of times to restore the state manually. This is a bad idea because in RNG heavy games your Random generator state could theoretically get into the billions of calls, meaning you would—according to them—need to iterate a billion times to restore the state of the last play session during each startup. Granted, this may still only take a second, tops, but it's still too dirty in my opinion, especially when you could simply extract the current state of the Random Generator and reload it when required, and only taking up 1 array (59 indices of memory).
This is just an idea, so take from my code what you will.
Here is the full source, which is much too large to post here:
GrimoireRandom.cs
And for anyone who just wants the implementation for the question, I will post it here.
public int[] GetState()
{
int[] state = new int[59];
state[0] = _seed;
state[1] = _inext;
state[2] = _inextp;
for (int i = 3; i < this._seedArray.Length; i++)
{
state[i] = _seedArray[i - 3];
}
return state;
}
public void LoadState(int[] saveState)
{
if (saveState.Length != 59)
{
throw new Exception("GrimoireRandom state was corrupted!");
}
_seed = saveState[0];
_inext = saveState[1];
_inextp = saveState[2];
_seedArray = new int[59];
for (int i = 3; i < this._seedArray.Length; i++)
{
_seedArray[i - 3] = saveState[i];
}
}
My code is completely stand-alone, besides the DiceType enumeration, and the OpenTK Vector3 struct. Both of those functions can just be deleted and it will work for you.
回答6:
Store the amount of times the random number generator ran like Xi Huan
wrote.
Then simply loop to restore the old state.
Random rand= new Random();
int oldRNGState = 439394;
for(int i = 1; i < oldRNGState-1; i++) {
rand.Next(1)
}
Now just do
int lastOldRNGValue = rand.Next(whateverValue);
There is no way around this you have to loop to get back to where you left off.