字典查找抛出“索引超出数组界限”的异常。

8

我收到了一份错误报告,似乎来自以下代码:

public class AnimationChannelCollection : ReadOnlyCollection<BoneKeyFrameCollection>
{
        private Dictionary<string, BoneKeyFrameCollection> dict =
            new Dictionary<string, BoneKeyFrameCollection>();

        private ReadOnlyCollection<string> affectedBones;

       // This immutable data structure should not be created by the library user
        internal AnimationChannelCollection(IList<BoneKeyFrameCollection> channels)
            : base(channels)
        {
            // Find the affected bones
            List<string> affected = new List<string>();
            foreach (BoneKeyFrameCollection frames in channels)
            {
                dict.Add(frames.BoneName, frames);
                affected.Add(frames.BoneName);
            }
            affectedBones = new ReadOnlyCollection<string>(affected);

        }

        public BoneKeyFrameCollection this[string boneName]
        {           
            get { return dict[boneName]; }
        }
}

这是读取字典的调用代码:
public override Matrix GetCurrentBoneTransform(BonePose pose)
    {
        BoneKeyFrameCollection channel =  base.AnimationInfo.AnimationChannels[pose.Name];       
    }

这是创建字典的代码,发生在启动时:
// Reads in processed animation info written in the pipeline
internal sealed class AnimationReader :   ContentTypeReader<AnimationInfoCollection>
{
    /// <summary> 
    /// Reads in an XNB stream and converts it to a ModelInfo object
    /// </summary>
    /// <param name="input">The stream from which the data will be read</param>
    /// <param name="existingInstance">Not used</param>
    /// <returns>The unserialized ModelAnimationCollection object</returns>
    protected override AnimationInfoCollection Read(ContentReader input, AnimationInfoCollection existingInstance)
    {
        AnimationInfoCollection dict = new AnimationInfoCollection();
        int numAnimations = input.ReadInt32();

        /* abbreviated */

        AnimationInfo anim = new AnimationInfo(animationName, new AnimationChannelCollection(animList));

    }
}

错误信息如下:

索引超出了数组范围。

行:0

位置:System.Collections.Generic.Dictionary`2.FindEntry(TKey key)

位置:System.Collections.Generic.Dictionary`2.get_Item(TKey key)

位置:Xclna.Xna.Animation.InterpolationController.GetCurrentBoneTransform(BonePose pose)

我本应该得到一个带有错误键的KeyNotFoundException,但是却收到了“索引超出了数组范围”的异常。我不明白这个代码中如何会出现这种异常?
另外,这段代码是单线程运行的。

2
代码无法编译。你肯定是想说 return dict[boneName]; - David L
1
getter 的返回值在哪里? - Lloyd
2
此外,一旦添加适当的返回语句,我就可以正确地接收到 KeyNotFoundException。你需要更新你的代码以正确重现该问题,因为目前它不能。 - David L
1个回答

36

当文档说明不应抛出错误时,对于字典(或命名空间System.Collections中的任何内容)发生 "Index was outside the bounds of the array." 错误,总是由您违反了线程安全而引起。

System.Collections命名空间中的所有集合都只允许以下两个操作之一:

  • 不限数量的并发读取者和0个写入者。
  • 0个读取者和1个写入者。

您必须使用ReaderWriterLockSlim同步访问字典的所有操作,以实现上述精确行为。

private Dictionary<string, BoneKeyFrameCollection> dict =
            new Dictionary<string, BoneKeyFrameCollection>();
private ReaderWriterLockSlim dictLock = new ReaderWriterLockSlim();

public BoneKeyFrameCollection this[string boneName]
{           
    get 
    { 
        try
        {
            dictLock.EnterReadLock();
            return dict[boneName]; 
        }
        finally
        {
            dictLock.ExitReadLock();
        }
    }
}


 public void UpdateBone(string boneName, BoneKeyFrameCollection col)
 {  
    try
    {
        dictLock.EnterWriteLock();
        dict[boneName] = col; 
    }
    finally
    {
        dictLock.ExitWriteLock();
    }
 }

或者用ConcurrentDictionary<string, BoneKeyFrameCollection>替换您的Dictionary<string, BoneKeyFrameCollection>

private ConcurrentDictionary<string, BoneKeyFrameCollection> dict =
            new ConcurrentDictionary<string, BoneKeyFrameCollection>();

 public BoneKeyFrameCollection this[string boneName]
 {           
    get 
    { 
        return dict[boneName];
    }
 }

 public void UpdateBone(string boneName, BoneKeyFrameCollection col)
 {  
    dict[boneName] = col;
 }

更新: 我真的不明白你展示的代码是如何导致这个问题的。这里是引发抛出异常的函数的源代码

private int FindEntry(TKey key) {
    if( key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null) {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
        }
    }
    return -1;
}
唯一会抛出 ArgumentOutOfRangeException 异常的情况是尝试对不合法的记录在bucketsentries中进行索引。
因为您的键是一个string,而且字符串是不可变的,我们可以排除在将键放入字典后更改键的hashcode值。剩下的只有一个调用buckets[hashCode%buckets.Length]和几个调用entries[i]buckets[hashCode%buckets.Length] 失败的唯一方法是在调用buckets.Length属性和 this[int index]索引器之间替换了buckets实例。 buckets被替换的唯一时机是内部由 Insert 调用的 Resize 函数,在构造函数/第一次调用 Insert 时调用 Initialize,或在调用 OnDeserialization 时调用。

Insert 被调用的唯一位置是 this[TKey key] 的 setter、公开的 Add 函数和在 OnDeserialization 中。要使buckets被替换,唯一的方法就是我们正在同时对三个列出的函数之一进行调用,而另一个线程在 buckets[hashCode%buckets.Length] 调用期间进行 FindEntry 调用。

唯一可能出现问题的 entries[i] 调用方式是如果 entries 被替换(遵循与 buckets 相同的规则),或者我们得到了一个错误的值 i。获得 i 的错误值的唯一方法是 entries[i].next 返回一个错误值。从 entries[i].next中获取错误值的唯一方法是在 InsertResizeRemove 进行并发操作。

我能想到的唯一办法要么是 OnDeserialization 调用出现问题,在反序列化之前就有了坏数据,要么是 AnimationChannelCollection 中还有其他影响您未向我们展示的字典的代码。

动画在启动时加载到只读集合中。但是错误发生在正常的帧更新期间,在游戏会话进行了一段时间后。在游戏加载后,集合不会被修改。因此,我不知道会导致异常的写入器应该来自哪里。 - Rye bread
展示初始加载作为问题的编辑方式。问题很可能出在这里。我从未见过这个问题除了“我在集合上使用了多个并发写入器”之外还有其他根本原因。 - Scott Chamberlain
@user1919998,它可能是唯一的其他事情就是,如果您正在使用的键在集合的键中停留时更改了自己的HashCode().Equals()行为。但是,既然您的键是一个string,我认为这极不可能,除非您在创建字典时使用了一些奇怪的非标准IEqualityComparer<string> - Scott Chamberlain
我只在构造函数中添加了展示字典如何被写入的代码。 - Rye bread
@user1919998,我在帖子中添加了一次重大编辑,展示了我的思考过程。这很可能是由于OnDeserialization调用时出现了错误数据,或者存在其他访问字典但你没有向我们展示的因素。 - Scott Chamberlain
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接