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
This is because structs are value types + your
_enumerator
isreadonly
struct. So, when you callBasicEnumerator.MoveNext()
, you get the copy of the internal_enumerator
struct, then call theMoveNext()
for this copy then throw the copy away. The original struct remains unchanged and thus it’sCurrent
is still -1.To fix the problem, remove the readonly qualifier
The mistake is here
Since it’s
readonly
, the code forMoveNext
is doing a defensive copy-on-access, and rightly so, to prevent any changes to the underlying struct.Just remove the
readonly
and it worksdotnetfiddle