从 2D 矩阵中切片 Span<T> 行 - 不确定为什么会这样

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

我一直在寻找一种方法来从 2D 矩阵中提取切片,而无需实际重新分配复制内容,

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));
}

我用这个简单的单元测试检查了这个方法,显然它有效:

[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));
}

虽然我觉得这不对。我的意思是,我想了解幕后到底发生了什么,因为 ref m[x, y] 部分无法说服我。

由于二维数组中的 this[int x, int y] 方法只是返回一个值而不是引用,因此运行时如何获取对矩阵内该位置值的实际引用?

ref 修饰符不应该只获得对返回到方法的 float 值的 本地副本 的引用,而不是引用存储在矩阵中的实际值?我的意思是,否则 methods/parameters 和 ref returns 将毫无意义,事实并非如此。

我查看了测试方法的 IL 并注意到了这一点:

现在,我不是 100% 确定,因为我不太擅长阅读 IL,但是 ref m[x, y] 调用是否被转换为对其他 Address 方法的调用,我想这只是 returns 一个 ref 值本身?

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?

我的意思是,我只是通过查看 IL 注意到它,但我不知道它的存在,也不知道为什么之前的代码能正常工作,此时我想知道默认库中有多少很棒的东西而没有提示它适用于普通开发人员。

谢谢!

在我看来,您的困惑症结就在这里:

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?

您似乎误以为数组的索引器语法与其他类型的索引器语法完全相同。但事实并非如此。数组的索引器是 .NET 中的特例,被视为 变量,而不是 属性 或一对方法。

例如:

void M1()
{
    int[] a = { 1, 2, 3 };

    M2(ref a[1]);
    Console.WriteLine(string.Join(", ", a);
}

void M2(ref int i)
{
    i = 17;
}

产量:

1, 17, 3

这是有效的,因为表达式 a[1] 不是 对某个索引器 getter 的调用,而是描述了一个 变量 物理上位于给定数组的第二个元素中。

同样,当您调用 DangerousCreate() 并传递 ref m[row, 0] 时,您传递的是对 [row, 0] 数组中 m 数组元素的变量的引用.

由于传递的是对实际内存位置的引用,因此其余部分应该不足为奇。也就是说,Span<T> class 能够使用该地址来包装原始数组的特定子集,而无需分配任何额外的内存。

标准一维 (SZ) 数组具有三个操作码 - ldelemstelemldelema。它们表示可以对 变量 执行的操作 - 获取其值、设置其值以及获取对它的引用。 a[i] 语法只是翻译成代表您对元素所做的任何事情。其他变量具有相似的操作码(ldlocstlocldlocaldfldstfldldflda 等)

但是,这些操作码不能用于多维数组。引用 ECMA-335:

For one-dimensional arrays that aren’t zero-based and for multidimensional arrays, the array class provides a Get method.

For one-dimensional arrays that aren’t zero-based and for multidimensional arrays, the array class provides a StoreElement [sic] method

For one-dimensional arrays that aren’t zero-based and for multidimensional arrays, the array class provides an Address method.

StoreElement 方法已重命名为 Set,但这仍然有效。访问多维数组的元素会转换为您对它们执行的任何操作。

这三个方法具有这些签名:

instance int32 int32[0...,0...]::Get(int32, int32)
instance void int32[0...,0...]::Set(int32, int32, int32)
instance int32& int32[0...,0...]::Address(int32, int32)

这些内部方法由 CLR 实现。注意最后一个方法引用 returned。虽然最近才向 C# 添加了 return 引用的功能,但 CLI 从一开始就支持它。

另请注意,在任何时候都不会涉及索引器。事实上,数组甚至没有索引器,因为那是 C# 的东西并且它不足以实现变量的所有操作,因为缺少 get reference 访问器。

总而言之,数组上的 a[x] 和非数组(任何具有索引器的对象)上的 a[x] 大量 不同的东西.

顺便说一下,DangerousCreate 也可以工作,这要归功于这个声明(又是 ECMA-335):

Array elements shall be laid out within the array object in row-major order (i.e., the elements associated with the rightmost array dimension shall be laid out contiguously from lowest to highest index). The actual storage allocated for each array element can include platform-specific padding.