首页 > 解决方案 > 为什么 System.Random.Sample 会抛出 IndexOutOfRangeException?

问题描述

我在 Unity (2017.1.1f1) 中调用Next()了一个 .NET实例。它从受保护的函数Sample()System.Random内部抛出一个,它不接受任何参数并返回 0 和 1 之间的双精度值。什么可能导致这种行为?IndexOutOfRangeException

这是详细的例外情况

System.IndexOutOfRangeException:数组索引超出范围。

在 /Users/builduser/buildslave/mono/build/mcs/class/corlib/System/Random.cs:91 中的 System.Random.Sample () [0x0003e]

在 System.Random.Next (Int32 maxValue) [0x00017] 在 /Users/builduser/buildslave/mono/build/mcs/class/corlib/System/Random.cs:112

在 Quicksilver.SysIRand.RandInt (Int32 max_exclusive) [0x00008] 在 F:\SVNHome\gemrush\GemRush\Assets\Source\Shared\Utility\IRand.cs:38

在 Quicksilver.IEnumerableExt.SelectRandom[Skill](IEnumerable`1 集合,IRand rand,Int32 计数)[0x00070] 在 F:\SVNHome\gemrush\GemRush\Assets\Source\Shared\Utility\IEnumerableExt.cs:61

(这上面还有另外 13 层调用栈)

这是一个多线程环境,但每个线程都有自己专用的 System.Random 实例。从下面的代码中可以看出,传递给 Next() 的参数必须为 1 或更高。

这个错误在一个复杂的自动化测试脚本中被抛出了大约 1 个小时,因此多次运行以测试理论是昂贵的,并且任何改变 RNG 调用方式的修改都将阻止重现。(如果错误以某种方式涉及线程之间的意外交互,那么它可能根本无法重现。)

由于它在测试脚本中运行了一个小时,因此该方法的绝大多数调用一定不会引发错误。

直接使用随机数的函数在这里:

    // Chooses count items at random from the enumeration and returns them in an array
    // The order of selected items within the array is also random
    // If the collection is smaller than count, the entire collection is returned (in random order)
    public static T[] SelectRandom<T>(this IEnumerable<T> collection, IRand rand, int count = 1)
    {
        if (count <= 0) return new T[0];    // Optimization for trivial case

        T[] keep = new T[count];
        int found = 0;
        foreach (T item in collection)
        {
            if (found < count)
            {
                // Take the first #count items, in case that's all there are

                // Move a random item of those found so far (including the new one)
                // to the end of the array, and insert the new one in its place
                int r = rand.RandInt(found + 1);
                keep[found++] = keep[r];
                keep[r] = item;
            }
            else
            {
                // Random chance to replace one of our previously-selected elements
                ++found;
                if (rand.RandInt(found) < count)    // probability desired/found
                {
                    // Replace a random previously-selected element
                    int r = rand.RandInt(count);
                    keep[r] = item;
                }
            }
        }
        if (found < count)
        {
            // The collection was too small to get everything requested;
            // Make a new, smaller array containing everything in the collection
            T[] all = new T[found];
            Array.Copy(keep, all, found);
            return all;
        }
        return keep;
    }

从这一行抛出错误:

                if (rand.RandInt(found) < count)    // probability desired/found

IRand 是 System.Random 的一个非常薄的包装器的接口;IRand.RandInt() 只返回 Random.Next()。

编辑

以下是 Random 实例的创建和分发方式:

   private void Start()
    {
        SysIRand[] rngs = new SysIRand[parallelTesters];
        if (parallelTesters > 0) rngs[0] = new SysIRand(new System.Random(548913));
        if (parallelTesters > 1) rngs[1] = new SysIRand(new System.Random(138498));
        if (parallelTesters > 2) rngs[2] = new SysIRand(new System.Random(976336));
        if (parallelTesters > 3) rngs[3] = new SysIRand(new System.Random(793461));
        if (parallelTesters > 4) rngs[4] = new SysIRand(new System.Random(648791));
        if (parallelTesters > 5) rngs[5] = new SysIRand(new System.Random(348916));
        if (parallelTesters > 6) rngs[6] = new SysIRand(new System.Random(8467168));
        if (parallelTesters > 7) rngs[7] = new SysIRand(new System.Random(6183569));
        for (int i = 8; i < parallelTesters; ++i)
        {
            rngs[i] = new SysIRand(new System.Random(7 * i * i + 8961 * i + 129786));
        }

        for (int t = 0; t < parallelTesters; ++t)
        {
            SysIRand rand = rngs[t];

            IBot bot = BotFactory.DrawBot(rand);

            BotTester tester = new BotTester(rand, bot);
            tester.testerID = t + 1;
            tester.OnMessage += (str) => UponMessage(tester.testerID + " ~ " + str);
            tester.OnError += (str) => UponError(tester.testerID + " ~ " + str);
            tester.OnGameAborted += UponGameAborted;
            tester.OnDebugMoment += UponDebugMoment;

            testers.Add(tester);
        }

(在这次运行中,parallelTesters值为 3)

每个都BotTester专门使用传递给其构造函数的 Random 实例。每个线程都是私有的BotTester,从BotTester.RunGame()

    public bool RunGame(GameMode mode, int maxGames = 1, long maxMilliSeconds = 100000000, int maxRetries = 5000)
    {
        if (threadRunning) return false;
        thread = new Thread(() => ThreadedRunGame(mode, maxGames, maxMilliSeconds, maxRetries));
        thread.Start();
        return true;
    }

标签: c#unity3d

解决方案


唯一有意义的解释是您认为您正在访问Random()实例线程安全,用您自己的话来说,每个线程都有自己的Random()实例,但看起来您正在访问同一个Random()实例,而它仍在计算中间。这是 Unity 中的实现。Sample()简单地调用InternalSample()

private int InternalSample()
{
  int inext = this.inext;
  int inextp = this.inextp;
  int index1;
  if ((index1 = inext + 1) >= 56)
    index1 = 1;
  int index2;
  if ((index2 = inextp + 1) >= 56)
    index2 = 1;
  int num = this.SeedArray[index1] - this.SeedArray[index2];
  if (num < 0)
    num += int.MaxValue;
  this.SeedArray[index1] = num;
  this.inext = index1;
  this.inextp = index2;
  return num;
}

如您所见,您可以获得的地方IndexOutOfRangeException是有限的,即当您访问时this.SeedArray。这是 SeedArray 的定义。

public class Random
{
    private int[] SeedArray = new int[56];
....
}

它已经分配给 56 个元素,在InternalSample方法的实现中,您可以看到对于每次调用index1index2并且始终限制为最多55,除非InternalSample方法被多次调用。


推荐阅读