diff --git a/src/cs/MonoGame.Extended/Collections/Bag.cs b/src/cs/MonoGame.Extended/Collections/Bag.cs index 292c1d19..868b6d72 100644 --- a/src/cs/MonoGame.Extended/Collections/Bag.cs +++ b/src/cs/MonoGame.Extended/Collections/Bag.cs @@ -153,40 +153,63 @@ namespace MonoGame.Extended.Collections Array.Copy(oldElements, 0, _items, 0, oldElements.Length); } - IEnumerator IEnumerable.GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Get the for this . + /// + /// + /// + /// Use this method preferentially over while enumerating via foreach + /// to avoid boxing the enumerator on every iteration, which can be expensive in high-performance environments. + /// + public BagEnumerator GetEnumerator() { return new BagEnumerator(this); } - IEnumerator IEnumerable.GetEnumerator() + /// + /// Enumerates a Bag. + /// + public struct BagEnumerator : IEnumerator { - return new BagEnumerator(this); - } - - internal struct BagEnumerator : IEnumerator - { - private volatile Bag _bag; + private readonly Bag _bag; private volatile int _index; + /// + /// Creates a new for this . + /// + /// public BagEnumerator(Bag bag) { _bag = bag; _index = -1; } - T IEnumerator.Current => _bag[_index]; - object IEnumerator.Current => _bag[_index]; + readonly T IEnumerator.Current => _bag[_index]; + readonly object IEnumerator.Current => _bag[_index]; + + /// + /// Gets the element in the at the current position of the enumerator. + /// + public readonly T Current => _bag[_index]; + + /// public bool MoveNext() { return ++_index < _bag.Count; } - public void Dispose() + /// + public readonly void Dispose() { } - public void Reset() + /// + public readonly void Reset() { } } diff --git a/src/cs/Tests/MonoGame.Extended.Tests/Collections/BagTests.cs b/src/cs/Tests/MonoGame.Extended.Tests/Collections/BagTests.cs new file mode 100644 index 00000000..471743de --- /dev/null +++ b/src/cs/Tests/MonoGame.Extended.Tests/Collections/BagTests.cs @@ -0,0 +1,42 @@ +using MonoGame.Extended.Collections; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +namespace MonoGame.Extended.Tests.Collections +{ + public class BagTests + { + [Fact] + public void Bag_Enumeration_Does_Not_Allocate() + { + var bag = new Bag(); + for (int i = 0; i < 100; i++) bag.Add(i); + // ensure we have plenty of memory and that the heap only increases for the duration of this test + Assert.True(GC.TryStartNoGCRegion(Unsafe.SizeOf.BagEnumerator>() * 1000)); + var heapSize = GC.GetAllocatedBytesForCurrentThread(); + + // this should NOT allocate + foreach (int i in bag) + { + // assert methods cause the NoGCRegion to fail, so do this manually + if (GC.GetAllocatedBytesForCurrentThread() != heapSize) + Assert.True(false); + } + + // sanity check: this SHOULD allocate + foreach (int _ in (IEnumerable)bag) + { + // assert methods cause the NoGCRegion to fail, so do this manually + if (GC.GetAllocatedBytesForCurrentThread() == heapSize) + Assert.True(false); + } + + GC.EndNoGCRegion(); + } + + } +}