Home > Enterprise >  C# Unity foreach loop exits early without break?
C# Unity foreach loop exits early without break?

Time:10-19

I'm trying to count items (int id) in a list. Doing it so:

int currentId = -1;
int count = 0;
foreach (var item in rawItemList) {
    UnityEngine.Debug.LogFormat("Item {0}", item);
    if (currentId != item) {
        AddItem(currentId, count);
        count = 0;
    }
    currentId = item;
    count  ;
    UnityEngine.Debug.LogFormat("Count {0}", count);
}

And here's the AddItem function:

void AddItem(int itemId, int count) {
    UnityEngine.Debug.LogFormat("Add item {0} count {1}", itemId, count);
    if (count == 0) return;
    items.Add(itemId);
    counts.Add(count);
}

rawItemList, items and counts are all NativeList<int>.

There are 7 elements in rawItemList - {0, 0, 2, 2, 3, 4, 4}. Printing those before and after above loop works fine (prints all the elements).

The issue I'm having is that after second AddItem call, the foreach loop exits, not even printing the rest of elements in rawItemList. Maybe worth mentioning, this code happens in a constructor. Is there a bug in here I'm not seeing?

Console log:

Item 0
Add item -1 count 0
Count 1
Item 0
Count 2
Item 2
Add item 0 count 2
Count 1
// Loop exits here

EDIT: Just checked with standard for loop. Works fine this way. What's wrong with foreach?

EDIT 2: Inserting full code. The last item is added outside of the loop.

// public struct CountedItemList ...

public NativeList<int> items;
public NativeList<int> counts;

public CountedItemList(NativeList<int> rawItemList) {
    items = new NativeList<int>(Allocator.Temp);
    counts = new NativeList<int>(Allocator.Temp);
    if (rawItemList.Length == 0) return;
    rawItemList.Sort();
    int currentId = -1;
    int count = 0;
    foreach (var item in rawItemList) {
        UnityEngine.Debug.LogFormat("Item {0}", item);
        if (currentId != item) {
            AddItem(currentId, count);
            count = 0;
        }
        currentId = item;
        count  ;
        UnityEngine.Debug.LogFormat("Count {0}", count);
    }
    AddItem(currentId, count);
}

It's called like this from another script:

NativeList<int> rawItemList = new NativeList<int>(Allocator.Temp);

// Populate items

var cil = new CountedItemList(rawItemList);
rawItemList.Dispose();

EDIT 3: In EDIT 1 I've replaced

foreach(var item in rawItemList) {

With

for(int i = 0; i < rawItemList.Length; i   {
    int item = rawItemList[i];

The rest of the code stayed the same, yet the behavior changed.

CodePudding user response:

As said pure logic wise I can not reproduce this.

However, I implemented more or less exactly what you have in Unity like this

public class Test : MonoBehaviour
{
    private CountedItemList cil;

    private void Start()
    {
        NativeList<int> rawItemList = new NativeList<int>(Allocator.Temp);

// Populate items
        rawItemList.Add(0);
        rawItemList.Add(0);
        rawItemList.Add(2);
        rawItemList.Add(2);
        rawItemList.Add(2);
        rawItemList.Add(3);
        rawItemList.Add(3);
        rawItemList.Add(4);

        cil = new CountedItemList(rawItemList);
        rawItemList.Dispose();
    }

    private void OnDestroy()
    {
        cil?.Dispose();
    }
}

public class CountedItemList : IDisposable
{
    public NativeList<int> items;
    public NativeList<int> counts;

    public CountedItemList(NativeList<int> rawItemList)
    {
        items = new NativeList<int>(Allocator.Temp);
        counts = new NativeList<int>(Allocator.Temp);

        if (rawItemList.Length == 0) return;

        rawItemList.Sort();

        var currentId = -1;
        var count = 0;

        //for(var i = 0; i < rawItemList.Length; i  )
        //{
        //    var item = rawItemList[i];
        foreach (var item in rawItemList)
        {
            UnityEngine.Debug.LogFormat("Item {0}", item);
            if (currentId != item)
            {
                AddItem(currentId, count);
                count = 0;
            }

            currentId = item;
            count  ;
            UnityEngine.Debug.LogFormat("Count {0}", count);
        }

        AddItem(currentId, count);
    }

    void AddItem(int itemId, int count)
    {
        UnityEngine.Debug.LogFormat("Add item {0} count {1}", itemId, count);
        if (count == 0) return;
        items.Add(itemId);
        counts.Add(count);
    }

    public void Dispose()
    {
        items.Dispose();
        counts.Dispose();
    }
}

Which results in a pretty not ignorable exception

ObjectDisposedException: The Unity.Collections.NativeList`1[System.Int32] has been deallocated, it is not allowed to access it
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <86acb61e0d2b4b36bc20af11093be9a5>:0)
Unity.Collections.NativeArray`1[T].CheckElementReadAccess (System.Int32 index) (at <86acb61e0d2b4b36bc20af11093be9a5>:0)
Unity.Collections.NativeArray`1[T].get_Item (System.Int32 index) (at <86acb61e0d2b4b36bc20af11093be9a5>:0)
Unity.Collections.NativeArray`1 Enumerator[T].get_Current () (at <86acb61e0d2b4b36bc20af11093be9a5>:0)
CountedItemList..ctor (Unity.Collections.NativeList`1[T] rawItemList) (at Assets/Scripts/ExamplePart.cs:24)
Test.Start () (at Assets/Scripts/Test.cs:24)

pointing to the line

foreach (var item in rawItemList)

I think the explanation is something like

The NativeList<int>.GetEnumerator used by foreach seems to be iterating asynchronously. You immediately do

var cil = new CountedItemList(rawItemList);
rawItemList.Dispose();

so the rawItemList.Dispose(); seems to get called before the iteration via foreach is finished.

On the other hand for uses no special enumerator but synchronous index accesses so here it is ensured hat the constructor is finished before the list is diposed.

  • Related