NativeHashMap Enumerator is broken if element is deleted from it

Hi to all! I tried to iterate over NativeHashMaps using GetEnumerator(). And everything worked good untill I tried to remove an element from map

note: I dont try to remove element while iterating. I remove element, and then iterate.

  1. the enumerator returns the removed element, but iterates, if you remove just 1 element
  2. the enumerator returns the same result every time, if you remove 2 and more elements from it.

My Collections package version is 0.11.0-Preview-17

My examples of breaking the enumerator are here:

 private void Test1()
    {
        Debug.Log("FIRST TEST STARTED");
        //prepare test HashMap and fill it with 5 elemebts
        var elementsCount = 5;
        var testHashMap = new NativeHashMap<int, int>(elementsCount, Allocator.Temp);
        for (int i = 0; i < elementsCount; i++)
            testHashMap.Add(i, UnityEngine.Random.Range(0, 100));

        //Iterate over it with enumerator as much as you want. And it works as it should
        for (int i = 0; i < 2; i++)
        {
            Debug.Log(i + 1 + " attempt to iterate:");
            using (var enumerator = testHashMap.GetEnumerator())
                while (enumerator.MoveNext())
                    Debug.Log($"{enumerator.Current.Key} {enumerator.Current.Value}");
        }

        // lets remove element
        var elementToRemove = 2;
        testHashMap.Remove(elementToRemove);

        //the enumerator values are incorrect. it works like another element was removed
        Debug.Log("Attempt to iterate after remove element:");
        using (var enumerator = testHashMap.GetEnumerator())
            while (enumerator.MoveNext())
            {
                if (enumerator.Current.Key == elementToRemove)
                {
                    Debug.LogError("This key was removed, but we've got it");
                }
                Debug.Log($"{enumerator.Current.Key} {enumerator.Current.Value}");

            }

        testHashMap.Dispose();
        Debug.Log("FIRST TEST ENDED");
    }
 private void Test2()
    {
        Debug.Log("SECOND TEST STARTED");
        //prepare test HashMap and fill it with 5 elemebts
        var elementsCount = 5;
        var testHashMap = new NativeHashMap<int, int>(elementsCount, Allocator.Temp);
        for (int i = 0; i < elementsCount; i++)
            testHashMap.Add(i, UnityEngine.Random.Range(0, 100));

        //Iterate over it with enumerator as much as you want. And it works as it should
        for (int i = 0; i < 2; i++)
        {
            Debug.Log(i + 1 + " attempt to iterate:");
            using (var enumerator = testHashMap.GetEnumerator())
                while (enumerator.MoveNext())
                    Debug.Log($"{enumerator.Current.Key} {enumerator.Current.Value}");
        }

        // lets remove 2 elements
        //removing two elements will tottally break enumerator
        for (int i = 0; i < 2; i++)
            testHashMap.Remove(i);

        Debug.Log("Attempt to iterate after remove element:");
        int breakCounter = 0;
        using (var enumerator = testHashMap.GetEnumerator())
            while (enumerator.MoveNext())
            {
                Debug.Log($"{enumerator.Current.Key} {enumerator.Current.Value}");
                breakCounter++;
                if (breakCounter > 100) //break here, cause we iterate over and over :)
                    break;
            }

        testHashMap.Dispose();
        Debug.Log("SECOND TEST ENDED");
    }

1 Like

Breaking is expected behaviour if you ask me (though the method of breaking is interesting). That’s how nearly all Enumerator work; they break if you make changes

var list = new List<int>();

foreach(var i in list)
{
    list.Remove(i); // break
}

For me too, but your example is incorrect. You remove element while iterating. I dispose enumerator, and then remove element, and then create a new enumerator and try to iterate

This situation makes NativeCollections to be “add only” collection :frowning:
I dont change collection while iterating. So I expected it will work like Dictionaries and HashSets

for example, this code works and does not break enumerator

        Dictionary<int, int> dictionary = new Dictionary<int, int>();
        for (int i = 0; i < 5; i++)
            dictionary.Add(i, i);

        using (var enumerator = dictionary.GetEnumerator())
        {
            while (enumerator.MoveNext())
                Debug.Log(enumerator.Current.Key + " " + enumerator.Current.Value);
        }

        dictionary.Remove(2);

        using (var enumerator = dictionary.GetEnumerator())
        {
            while (enumerator.MoveNext())
                Debug.Log(enumerator.Current.Key + " " + enumerator.Current.Value);
        }
1 Like

If you’re not making changes during enumeration then I agree it’s a bug (sorry I only quickly looked at your code.)

2 Likes

Yes, I dont change anything while iterating, I know that changing collection while iterating is not allowed. :slight_smile: I will add this information to the beginning of the post.
modification while iterating is the most obvious thing to cause bug, but this is not that situation :slight_smile:

1 Like

Sorry for such a long example which is difficult to read, I’ve made a new one, shorter

        NativeHashMap<int, int> map = new NativeHashMap<int, int>(5, Allocator.Temp);
        for (int i = 0; i < 5; i++)
            map.Add(i, i);

        Debug.Log("Iterate before remove");
        using (var enumerator = map.GetEnumerator())
        {
            while (enumerator.MoveNext())
                Debug.Log(enumerator.Current.Key + " " + enumerator.Current.Value);
        }

        map.Remove(2);
        Debug.Log("Iterate after remove");
        using (var enumerator = map.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                if (enumerator.Current.Key == 2)
                    Debug.LogError("deleted element is returned");
                Debug.Log(enumerator.Current.Key + " " + enumerator.Current.Value);
            }
        }
        map.Dispose();
1 Like

I’ve also sent the bug report with this text:

  1. Enumerators of NativeCollections are broken, if you’ve ever removed elements from it.
    The removal of one elements does not break iterating over enumerator, but returns the deleted key in one of Currents.
    The removal of two elements and more will make enumerator iterate infinite and return the same Current element.
    2.Reproduction:
    Run tests in attached project
    First test:
    EnumeratorDoesNotReturnRemovedElementsTest:
  • creates NativeHashMap
  • fills it with 5 elements with keys 1,2,3,4,5
  • removes element with key “2”
  • calls GetEnumerator
  • iterates over enumerator
    Expected: the key 2 is not returned while iterating over enumerator
    Result: the key 2 is returned while iterating over enumerator
    Second test:
    EnumeratorInfiniteIterationTest
  • creates NativeHashMap
  • fills it with 5 elements with keys 1,2,3,4,5
  • removes elements two ekelements (key = 0, key = 1)
  • calls GetEnumerator
  • iterates over enumerator
    Expected: iteration count is less than one hundred iterations (infinite loop detected, cause there are only 3 elements)
    Result: Assert informs about error, cause 100 iterations passed

and test example

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Collections;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests
{
    public class NewTestScript
    {
        [Test]
        public void EnumeratorDoesNotReturnRemovedElementsTest()
        {
            NativeHashMap<int, int> map = new NativeHashMap<int, int>(5, Allocator.Temp);
            for (int i = 0; i < 5; i++)
                map.Add(i, i);

            int ELEMENT_TO_REMOVE = 2;      //we remove one element here
            map.Remove(ELEMENT_TO_REMOVE);

            using (var enumerator = map.GetEnumerator())
                while (enumerator.MoveNext())
                    Assert.AreNotEqual(ELEMENT_TO_REMOVE, enumerator.Current.Key);  //we removed key "2". lets check if enumerator returned it

            map.Dispose();
        }

        [Test]
        public void EnumeratorInfiniteIterationTest()
        {
            NativeHashMap<int, int> map = new NativeHashMap<int, int>(5, Allocator.Temp);
            for (int i = 0; i < 5; i++)
                map.Add(i, i);

            for (int i = 0; i < 2; i++)     //we remove two elements now
                map.Remove(i);

            int breaker = 0;
            using (var enumerator = map.GetEnumerator())
                while (enumerator.MoveNext())
                {
                    if (breaker++ > 100)    //break if iterating over and over
                        break;
                }

            Assert.Less(breaker, 100);      //check if there was infinite loop
            map.Dispose();
        }
    }
}
2 Likes

Case 1267807

Hi, MoveTo bug is already fixed. Will be in next release. I just ran with your unit tests, and both are green.

2 Likes

Tnank you so much! Great news, I will wait for this release

Marked thread as resolved