Search code examples
c#.netcilc#-7.2

Slicing a Span<T> row from a 2D matrix - not sure why this works


I've been looking for a way to extract slices from a 2D matrix without having to actually reallocate-copy the contents, and

public static Span<float> Slice([NotNull] this float[,] m, int row)
{
    if (row < 0 || row > m.GetLength(0) - 1) throw new ArgumentOutOfRangeException(nameof(row), "The row index isn't valid");
    return Span<float>.DangerousCreate(m, ref m[row, 0], m.GetLength(1));
}

I've checked this method with this simple Unit tests and apparently it works:

[TestMethod]
public void Foo()
{
    float[,] m =
    {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 9.5f, 10, 11 },
        { 12, 13, 14.3f, 15 }
    };
    Span<float> s = m.Slice(2);
    var copy = s.ToArray();
    var check = new[] { 9, 9.5f, 10, 11 };
    Assert.IsTrue(copy.Select((n, i) => Math.Abs(n - check[i]) < 1e-6f).All(b => b));
}

This doesn't seem right to me though. I mean, I'd like to understand what's exactly happening behind the scenes here, as that ref m[x, y] part doesn't convince me.

How is the runtime getting the actual reference to the value at that location inside the matrix, since the this[int x, int y] method in the 2D array is just returning a value and not a reference?

Shouldn't the ref modifier only get a reference to the local copy of that float value returned to the method, and not a reference to the actual value stored within the matrix? I mean, otherwise having methods/parameters with ref returns would be pointless, and that's not the case.

I took a peek into the IL for the test method and noticed this:

enter image description here

Now, I'm not 100% sure since I'm not so great at reading IL, but isn't the ref m[x, y] call being translated to a call to that other Address method, which I suppose just returns a ref value on its own?

If that's the case, is there a way to directly use that method from C# code?

And is there a way to discover methods like this one, when available?

I mean, I just noticed that by looking at the IL and I had no idea it existed or why was the code working before, at this point I wonder how much great stuff is there in the default libs without a hint it's there for the average dev.

Thanks!


Solution

  • It seems to me that the crux of your confusion is here:

    Shouldn't the ref modifier only get a reference to the local copy of that float value returned to the method, and not a reference to the actual value stored within the matrix?

    You seem to be under the mistaken impression that the indexer syntax for an array works exactly the same as for other types. But it doesn't. An indexer for an array is a special case in .NET, and treated as a variable, not a property or pair of methods.

    For example:

    void M1()
    {
        int[] a = { 1, 2, 3 };
    
        M2(ref a[1]);
        Console.WriteLine(string.Join(", ", a);
    }
    
    void M2(ref int i)
    {
        i = 17;
    }
    

    yields:

    1, 17, 3

    This works because the expression a[1] is not a call to some indexer getter, but rather describes a variable that is physically located in the second element of the given array.

    Likewise, when you call DangerousCreate() and pass ref m[row, 0], you are passing the reference to the variable that is exactly the element of the m array at [row, 0].

    Since a reference to the actual memory location is what's being passed, the rest should be no surprise. That is, that the Span<T> class is able to then use that address to wrap a specific subset of the original array, without allocating any extra memory.