简体   繁体   English

如何在 C# 中保存随机生成器的状态?

[英]How to save the state of a Random generator in C#?

For testing purposes I'm creating random numbers with a given seed (ie 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.因此我需要能够将System.Random恢复到以前的状态。

Is there a way to extract a seed which I can use to recreate the random generator?有没有办法提取可用于重新创建随机生成器的种子?

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 .您可以在LINQPad 中测试此代码。

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);

}

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: System.Random不是密封的,它的方法是虚拟的,因此您可以创建一个类来计算生成的数字数量以跟踪状态,例如:

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

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.本质上,我使用 .NET 的 Random.cs 代码创建了自己的 Random 类。 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.我不仅添加了更多功能,而且还添加了一种方法来保存当前生成器状态并将其加载到只有 59 个索引的数组中。 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).最好这样做,而不是其他一些评论建议“迭代 x 次手动恢复状态。这是一个坏主意,因为在 RNG 重型游戏中,您的随机生成器状态理论上可以进入数十亿次调用, 根据他们的说法,这意味着您将需要迭代 10 亿次以在每次启动期间恢复上次播放会话的状态。当然,这可能仍然只需要一秒钟,顶部,但在我看来它仍然太脏,尤其是当您可以简单地提取随机生成器的当前状态并在需要时重新加载它时,并且只占用 1 个数组(59 个内存索引)。

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 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.除了 DiceType 枚举和 OpenTK Vector3 结构之外,我的代码是完全独立的。 Both of those functions can just be deleted and it will work for you.这两个功能都可以删除,它对你有用。

There is an alternative solution that (1) avoids the need to remember all previously generated numbers;有一个替代解决方案,(1) 避免需要记住所有先前生成的数字; (2) does not involve accessing the private fields of Random; (2) 不涉及访问Random的私有字段; (3) does not require serialization; (3) 不需要序列化; (4) does not require looping back through Random as many times as it had been called; (4) 不需要像调用过的那样多次循环返回 Random; and (5) does not require creating a replacement for the built-in Random class. (5) 不需要为内置的 Random 类创建替代品。

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.请注意,可以访问 Generator 属性以实际生成数字。

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);
}

Here is a refined version picking up from some of the answers here, just add this to your project.这是从这里的一些答案中提取的精炼版本,只需将其添加到您的项目中即可。

public class RandomState
    {

        private static Lazy<System.Reflection.FieldInfo> _seedArrayInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_seedArray", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static Lazy<System.Reflection.FieldInfo> _inextInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_inext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static Lazy<System.Reflection.FieldInfo> _inextpInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_inextp", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static System.Reflection.FieldInfo seedArrayInfo {get { return _seedArrayInfo.Value; }}
        private static System.Reflection.FieldInfo inextInfo { get { return _inextInfo.Value; } }
        private static System.Reflection.FieldInfo inextpInfo { get { return _inextpInfo.Value; } }

        private int[] seedState;
        private int inext;
        private int inextp;
        public static RandomState GetState(Random random)
        {
            var state = new RandomState() { seedState = ((int[])seedArrayInfo.GetValue(random)).ToArray(), inext = (int)inextInfo.GetValue(random), inextp = (int)inextpInfo.GetValue(random) };
            return state;
        }
        public static void SetState(Random random, RandomState state)
        {
            seedArrayInfo.SetValue(random, state.seedState.ToArray());
            inextInfo.SetValue(random, state.inext);
            inextpInfo.SetValue(random, state.inextp);
        }
    }
    public static class RandomExtensions
    {
        public static RandomState GetState (this System.Random random)
        {

            return RandomState.GetState(random);
        }
        public static void ApplyState (this System.Random random, RandomState state)
        {

            RandomState.SetState(random, state);
        }
    }

Example of using it, trying to replicate this .使用它的例子,试图复制这个

    public class Program
    {
        public static void Main (string[] args)
        {
            System.Random rnd = new System.Random (255);
            var firststate = rnd.GetState();
            Console.WriteLine("Saved initial state...");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom("Step ", rnd);
            var oldState = rnd.GetState();
            Console.WriteLine("Saved second state....");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom("Step ", rnd);
            PrintRandom("Step ", rnd);
            PrintRandom("Step ", rnd);
            rnd.ApplyState(oldState);
            Console.WriteLine("Re-applied second state state....");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            rnd.ApplyState(firststate);
            Console.WriteLine("Re-applied initial state state....");
            PrintRandom("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
        }

        static void PrintRandom (string label, Random rnd)
        {
            System.Console.WriteLine(string.Format ("{0} - RandomValue {1}", label, rnd.Next (1, 100)));
        }
    }

Output:输出:

Saved initial state...
Step  - RandomValue 94
Step  - RandomValue 64
Step  - RandomValue 1
Saved second state....
Step  - RandomValue 98
Step  - RandomValue 34
Step  - RandomValue 40
Step  - RandomValue 16
Step  - RandomValue 37
Re-applied second state state....
Step  - RandomValue 98
Step  - RandomValue 34
Step  - RandomValue 40
Step  - RandomValue 16
Step  - RandomValue 37
Re-applied initial state state....
Step  - RandomValue 94
Step  - RandomValue 64
Step  - RandomValue 1

Store the amount of times the random number generator ran like Xi Huan wrote.存储随机数生成器像Xi Huan写的那样运行的次数。

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.没有办法解决这个问题,你必须循环才能回到你离开的地方。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM