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.
the enumerator returns the removed element, but iterates, if you remove just 1 element
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");
}
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
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);
}
Yes, I dont change anything while iterating, I know that changing collection while iterating is not allowed. 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
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();
}
}
}