skip to Main Content

I was experimenting some stuff with the Span<T> type. My goal was to make an extension method to enumerate the splitting of a ReadOnlySpan<char>, to be used as follow :

var x = "foo,bar,baz".AsSpan();
var options = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;

foreach (var word in x.EnumerateSplit(',', options))
{
    // Should enumerate foo, bar and baz
}

But when I implement my own enumerator type that encapsulates a Span<Range>.Enumerator instance behind the scene, an IndexOutOfRangeException is thrown when accessing to the encapsulated enumerator Current property.

I tried to create the most basic enumerator that just have an encapsulation logic with no splitting mechanic :

public ref struct BasicEnumerator
{
    // Encapsulated enumerator
    private readonly Span<Range>.Enumerator _enumerator;

    public Range Current => _enumerator.Current;

    public BasicEnumerator(Span<Range>.Enumerator enumerator)
    {
        _enumerator = enumerator;
    }

    public bool MoveNext() => _enumerator.MoveNext();

    // Called by foreach
    public BasicEnumerator GetEnumerator() => this;
}

With this BasicEnumerator type, I was at least expecting this piece of code to work :

Span<Range> test = stackalloc Range[5];
var enumerator = new BasicEnumerator(test.GetEnumerator());

foreach (Range r in enumerator)
{
    Console.WriteLine(r);
}

But an IndexOutOfRangeException is thrown at the first iteration.

I also tried simpler without using a foreach :

Span<Range> test = stackalloc Range[5];
var enumerator = new BasicEnumerator(test.GetEnumerator());

if (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}

The IndexOutOfRangeException is still thrown when trying to access enumerator.Current.

Note that when I use the Span<Range>.Enumerator instead (in the same way), it works :

Span<Range> test = stackalloc Range[5];
Span<Range>.Enumerator enumerator = test.GetEnumerator();

if (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}

While debugging, I found that it is not working because the Span<Range>.Enumerator.MoveNext method does not increase the underlying index but still returns true :
Visual Studio debug screenshot

I don’t really know why it is doing that. I would say it is a reference problem but I don’t know why and how to resolve it. I tried to find documentation about a similar use case, without any success.

I’m using .NET 8.

2

Answers


  1. This is because structs are value types + your _enumerator is readonly struct. So, when you call BasicEnumerator.MoveNext(), you get the copy of the internal _enumerator struct, then call the MoveNext() for this copy then throw the copy away. The original struct remains unchanged and thus it’s Current is still -1.

    To fix the problem, remove the readonly qualifier

    //remove `readonly`
    private /*readonly*/ Span<Range>.Enumerator _enumerator;
    
    Login or Signup to reply.
  2. The mistake is here

    private readonly Span<Range>.Enumerator _enumerator;
    

    Since it’s readonly, the code for MoveNext is doing a defensive copy-on-access, and rightly so, to prevent any changes to the underlying struct.

    Just remove the readonly and it works

    private Span<Range>.Enumerator _enumerator;
    

    dotnetfiddle

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search