Tools to work with C# (.NET) managed references.
A continuous block of memory (such as an array or stack-alloced block) can be iterated over using pointer arithemtic.
Something similar can be implemented in C#
thanks to ref locals and ref returns.
If we define two methods, Begin
and End
internal static class Iter
{
public static ref readonly T Begin<T>(this ReadOnlySpan<T> @this) where T : unmanaged
=> ref @this[0];
public static ref readonly T End<T>(this ReadOnlySpan<T> @this) where T : unmanaged
=> ref Inc(in @this[@this.Length - 1]);
}
then an iteration over a continuous block of memory can happen in the following manner
public void Test_Inc_HeapArray()
{
var z = new ulong[1024];
ReadOnlySpan<ulong> x = new ulong[]
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
ulong sum = 0;
for (
ref readonly var itt = ref x.Begin();
!AreSame(in itt, in x.End());
itt = ref Inc(in itt)
)
sum += itt;
Assert.AreEqual(sum, 120);
}
Here, End()
points to the place 'after' the last element, which can be dangerous and needs testing, especially when garbage collection happens after the reference is obtained.
The logic of for
-loop is strighforward: intialize iterator variable with a reference to Begin()
, continue while itt
is not equal to End()
(can be replaced by IsAddressLessThan
for safety), at the end of each step increment itt
by 1 (using a shortcut to Add(in itt, 1)
).
The readonly
keyword allows for some additional compile-time checks, preventing from writing to the place, pointed by ref readonly
. With a number of helpers which wrap Unsafe.*
methods and provide overloads for ref readonly
aka in
parameters, the code looks clean.
The same can be done for e.g. multiple iterators:
for (
ref readonly var xItt = ref x.Begin(), yItt = ref y.Begin();
!AreSame(in xItt, in x.End()) && !AreSame(in yItt, in y.End());
xItt = ref Inc(in xItt), yItt = ref Inc(in yItt)
)
if (xItt != yItt)
throw new Exception();