【问题标题】:How to save the state of a Random generator in C#?如何在 C# 中保存随机生成器的状态?
【发布时间】:2013-10-30 23:44:39
【问题描述】:

出于测试目的,我正在使用给定的种子(即不基于当前时间)创建随机数。

因此整个程序是确定性的。

如果发生了什么事,我希望能够快速恢复事件“不久前”的某个点。

因此我需要能够将System.Random 恢复到以前的状态。

有没有办法提取种子,我可以用它来重新创建随机生成器?

【问题讨论】:

  • 您可以跟踪对 System.Random 的调用次数,这样您就可以知道故障发生的时间。要恢复状态,只需设置相同的原始种子,然后调用生成器正确的次数。
  • System.Random 被标记为可序列化。
  • 您可能会发现这个答案很有帮助:stackoverflow.com/a/8188878/2609288
  • 你可以编写一个自定义的 RNG。额外的好处是它可以产生比System.Random更好的输出质量。
  • 绝对只是序列化它,如 Baldrick 链接的答案。这确实是一个狡猾的计划。

标签: c# random


【解决方案1】:

根据answer given here,我写了一个小类来帮助保存和恢复状态。

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

您可以在LINQPad 中测试此代码。

【讨论】:

  • 这样,每次生成数字时,都必须存储随机数生成器的状态。否则,您将无法轻松跳回之前的状态。
  • 没错。另一种方法是你必须从头开始重新启动并调用它 N 次。这完全取决于什么是最容易处理的。
  • 如果您要为“转储”扩展方法添加代码,其他人可以复制/粘贴此代码。
  • 这是一个 LINQPad 扩展,不是我的,因此我的最后评论是您可以在 LINQPad 中测试该代码。无论如何,您不需要该扩展来使用代码,这只是为了显示保存和恢复状态的结果,这是您在实际代码中不会做的事情。
  • 虽然这是选择的答案,而且效果很好,但我觉得另一种方法可能更快,更容易动态存储在任何文件中,并且通常是更好的选择。我的答案已添加,位于下方。
【解决方案2】:

这是我想出来的:

基本上它提取私有种子数组。 您只需要小心恢复“未共享”数组即可。

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

}

【讨论】:

  • 这有以下好处: 1. 它看起来最简单,计算量也不大 2. 它将所有负载转移到很少执行的代码上,即Random 实例化以及当您需要实际状态时恢复。否则零开销。我还发现将 Random 重新插入自身的提议很巧妙,但我仍然更喜欢这个提议,因为它的性能提高了约 150 倍。
【解决方案3】:

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

这只是一个想法,所以从我的代码中获取你想要的。

这是完整的源代码,太大了,无法在此处发布:

GrimoireRandom.cs

对于任何只想解决问题的人,我会在这里发布。

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

我的代码是完全独立的,除了 DiceType 枚举和 OpenTK Vector3 结构。这两个功能都可以删除,它会为你工作。

【讨论】:

  • 谢谢。似乎可以很好地满足我的需要。除了保存和恢复游戏状态以确保一切一致之外,保存随机数生成器的状态对于录制我的游戏和重播游戏非常有帮助。
  • @BrienKing 真的很高兴我能帮上忙!编码愉快!
【解决方案4】:

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

示例用法:

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

}

输出:

81
73
4
81
73
4

【讨论】:

  • 我不认为这些论点会影响内部状态,但我不得不承认我没有测试过。
  • 这没用。它仅表明使用相同的随机种子生成相同的随机数。 OP 已经知道这一点。
  • @HansPassant 我知道 OP 知道使用相同种子的意义。他想知道一种方法来创建一个具有相同种子的随机数生成器,这样他就可以在事件“不久之前”的一个点,这就是我的回答所显示的。跨度>
【解决方案5】:

有一个替代解决方案,(1)避免需要记住所有先前生成的数字; (2) 不涉及访问 Random 的私有字段; (3) 不需要序列化; (4) 不需要像它被调用的那样多次通过 Random 循环; (5) 不需要为内置的 Random 类创建替换。

诀窍是通过生成一个随机数来获取状态,然后将随机数生成器重新设置为该值。然后,在未来,人们总是可以通过将随机数生成器重新设置为这个值来返回到这个状态。换句话说,我们在随机数序列中“烧掉”一个数字,目的是为了保存状态和重新播种。

实现如下。请注意,可以访问 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);
    }
}

这是一个简单的测试:

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

【讨论】:

  • 这将有效地经常为生成器重新播种。我不确定生成器的属性是否仍以这种方式保留。但是是的,这使得状态很容易保存。
  • 是的。当然,如果底层生成器足够随机,那么这也是随机的。但是有一个有趣的问题,这是否会放大底层算法中的任何缺陷。如果确实如此(我也不确定),那么问题有多大可能取决于重新播种的频率。
  • 如果在第一次和第二次调用 NextDouble() 之间没有保存状态,这种方法不会产生相同的序列;
  • 你永远无法“捕获”这里的状态。你只能设置它。能够在任何时候任意保存它需要每次你想要一个随机数时拨打 2 次电话。
【解决方案6】:

这是从这里的一些答案中挑选出来的精炼版本,只需将其添加到您的项目中即可。

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

使用示例,尝试复制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)));
        }
    }

输出:

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

【讨论】:

  • 使用反射意味着你有一个脆弱的解决方案,可能会停止使用下一个版本的 .NET 平台。
【解决方案7】:

存储随机数生成器运行的次数,如Xi Huan 所写。

然后简单地循环以恢复旧状态。

Random rand= new Random();
int oldRNGState = 439394;

for(int i = 1; i < oldRNGState-1; i++) {
    rand.Next(1)
}

现在就做

int lastOldRNGValue = rand.Next(whateverValue);

没有办法解决这个问题,你必须循环才能回到你离开的地方。

【讨论】:

  • 没有。下次再试。
  • @Sspoke 请在您的上方查看我的答案。您可以保存和恢复 Random 的内部状态。
猜你喜欢
  • 2011-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-01
  • 2011-01-22
  • 2014-08-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多