在C#中,多维数组(如double[,])与交错数组(double[][])不同。
多维数组是矩形数组,在内存中是连续存储的。
但是,Span
实际上,对于多维数组,我们可以将其视为一维数组来操作,因为它在内存中是连续的。
但是,这样做需要小心,因为数组的维度顺序和内存布局有关。
在C#中,多维数组按行优先顺序存储。也就是说,对于数组double[,] arr,元素arr[0,0]之后是arr[0,1],然后是arr[1,0],arr[1,1]等等。
我们可以使用MemoryMarshal.CreateSpan来创建一个Span,但请注意,这种方法需要引用System.Memory库,并且需要知道数组的起始地址和总长度。
但是,对于二维数组,我们可以使用以下方式:
1、使用fixed语句获取指针,然后使用指针操作。2、使用Span<T>来操作,但需要将二维数组转换为一维Span。
由于Span
另一种方法是使用MemoryMarshal.CreateSpan,但这个方法并不直接支持多维数组。我们可以使用以下技巧:
double[,] array = new double[10, 20];// 固定数组并获取指向第一个元素的指针
fixed (double* ptr = &array[0, 0])
{// 然后创建一个Span,长度为数组的总长度Span<double> span = new Span<double>(ptr, array.Length);// 现在我们可以使用span来访问数组,按一维方式for (int i = 0; i < span.Length; i++){span[i] = i;}
}
但是,请注意,这种方法将二维数组展开为一维,所以我们需要知道如何将二维索引转换为一维索引。如果我们想按二维方式访问,可以计算索引:
对于二维数组,索引[i, j]对应的一维索引是:i * 第二维的长度 + j。
因此,我们可以这样遍历:
int rows = array.GetLength(0);
int cols = array.GetLength(1);fixed (double* ptr = &array[0, 0])
{Span<double> span = new Span<double>(ptr, rows * cols);for (int i = 0; i < rows; i++){for (int j = 0; j < cols; j++){span[i * cols + j] = i * j;}}
}
但是,这种方法仍然需要fixed语句。我们是否能够完全避免fixed呢?
在C# 7.2及以上版本,我们可以使用Span来安全地访问数组,而不需要fixed,但仅限于一维数组。对于多维数组,没有直接的支持。因此,对于多维数组,我们仍然需要fixed来获取指针,然后创建Span。
另外,从.NET Core 3.0开始,我们可以使用MemoryMarshal.GetArrayDataReference方法来获取数组的引用,然后创建Span,而无需fixed。但是,这个方法同样适用于一维数组,对于多维数组,它返回的是第一个元素的引用,并且数组必须是连续存储的(多维数组就是连续的)。
因此,我们可以这样写:
double[,] array = new double[10, 20];// 获取数组数据的引用(不需要fixed,因为GetArrayDataReference内部会处理固定)
ref double dataRef = ref MemoryMarshal.GetArrayDataReference(array);// 然后创建一个Span,长度为数组的总长度
Span<double> span = MemoryMarshal.CreateSpan(ref dataRef, array.Length);// 然后按一维方式使用
for (int i = 0; i < span.Length; i++)
{span[i] = i;
}
但是,请注意,这种方法仍然需要将多维数组视为一维数组。而且,这种方法只在.NET Core 3.0及以上版本可用。
总结一下,对于多维数组,我们可以通过将其视为一维数组来使用Span,这样就可以避免显式使用fixed语句(在.NET Core 3.0及以上版本),但需要自己处理索引计算。
注意:这种方法对于交错数组(jagged array)不适用,因为交错数组的每一行都是独立的数组,不是连续的内存块。
因此,对于多维数组,使用Span替代fixed是可行的,但需要小心索引计算。如果你需要按多维方式访问,可以封装一个方法来简化索引访问。
例如,我们可以创建一个扩展方法:
public static class MultiArrayExtensions
{public static Span<T> AsSpan<T>(this T[,] array){if (array == null)throw new ArgumentNullException(nameof(array));ref T dataRef = ref MemoryMarshal.GetArrayDataReference(array);return MemoryMarshal.CreateSpan(ref dataRef, array.Length);}public static int GetIndex<T>(this T[,] array, int i, int j){return i * array.GetLength(1) + j;}
}
然后可以这样使用:
double[,] array = new double[10, 20];
Span<double> span = array.AsSpan();// 设置第2行第3列的元素为5.0
span[array.GetIndex(1, 2)] = 5.0;
但是,请注意,这种方法需要每次计算索引,可能会稍微降低性能,但提供了方便。
另外,在.NET 5及以上版本,还有另一个选择:使用MemoryMarshal.CreateSpan来直接创建二维数组的Span,但同样需要按一维方式处理。
所以,虽然Span可以用于多维数组,但使用起来不如一维数组直接。如果你需要高性能且避免fixed,可以考虑使用一维数组来表示多维数据,这样使用Span会更加自然。
例如,使用一维数组表示二维数组:
csharp
int rows = 10, cols = 20;
double[] array = new double[rows * cols];// 然后使用Span
Span<double> span = array.AsSpan();// 访问第i行第j列:index = i * cols + j
span[i * cols + j] = 1.0;
这样就可以完全避免fixed,并且使用Span更加直接。
对于多维数组(如 double[,]),使用 Span<T> 确实比指针更复杂,但仍然是可行的。让我详细解释几种处理方法:
1. 多维数组的内存布局
首先理解多维数组在内存中的布局:
double[,] matrix = new double[2, 3] {{ 1.0, 2.0, 3.0 },{ 4.0, 5.0, 6.0 }
};// 内存布局:行优先存储
// [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
2. 使用 MemoryMarshal 创建 Span
基本方法
using System;
using System.Runtime.InteropServices;public class MultiDimensionalSpanExample
{public void ProcessMultiDimensionalArray(){double[,] matrix = new double[3, 4];// 方法1:将多维数组视为一维 SpanSpan<double> flatSpan = MemoryMarshal.CreateSpan(ref matrix[0, 0], matrix.Length);// 现在可以像一维数组一样操作for (int i = 0; i < flatSpan.Length; i++){flatSpan[i] = i * 1.1;}// 验证结果Console.WriteLine($"matrix[1,2] = {matrix[1, 2]}"); // 应该是 6.6}
}
3. 手动索引计算
创建辅助方法
public static class MultiDimensionalSpanHelper
{// 将二维数组转换为 Span 并提供索引计算public static Span<T> AsSpan<T>(this T[,] array){return MemoryMarshal.CreateSpan(ref array[0, 0], array.Length);}// 计算二维索引在一维 Span 中的位置public static int CalculateIndex<T>(this T[,] array, int i, int j){return i * array.GetLength(1) + j;}// 通过二维索引访问 Spanpublic static ref T At<T>(this Span<T> span, T[,] array, int i, int j){int index = array.CalculateIndex(i, j);return ref span[index];}
}// 使用示例
public class UsageExample
{public void Demo(){double[,] matrix = new double[3, 4];Span<double> span = matrix.AsSpan();// 使用二维索引访问for (int i = 0; i < matrix.GetLength(0); i++){for (int j = 0; j < matrix.GetLength(1); j++){span.At(matrix, i, j) = i * 10.0 + j;}}// 验证Console.WriteLine($"matrix[2,3] = {matrix[2, 3]}"); // 应该是 23.0}
}
4. 更安全的封装方案
创建安全的包装器
public ref struct MatrixSpan<T> where T : unmanaged
{private readonly Span<T> _span;private readonly int _rows;private readonly int _cols;public MatrixSpan(T[,] array){_span = MemoryMarshal.CreateSpan(ref array[0, 0], array.Length);_rows = array.GetLength(0);_cols = array.GetLength(1);}public ref T this[int i, int j]{get{if (i < 0 || i >= _rows || j < 0 || j >= _cols)throw new IndexOutOfRangeException();return ref _span[i * _cols + j];}}public int Rows => _rows;public int Cols => _cols;// 获取原始 Span(如果需要批量操作)public Span<T> AsFlatSpan() => _span;
}// 使用示例
public class MatrixSpanExample
{public void ProcessMatrix(){double[,] matrix = new double[100, 200];// 创建 MatrixSpanvar matrixSpan = new MatrixSpan<double>(matrix);// 使用二维索引语法for (int i = 0; i < matrixSpan.Rows; i++){for (int j = 0; j < matrixSpan.Cols; j++){matrixSpan[i, j] = Math.Sin(i * 0.1) * Math.Cos(j * 0.1);}}// 批量操作(如果需要)Span<double> flat = matrixSpan.AsFlatSpan();for (int i = 0; i < flat.Length; i++){flat[i] *= 2.0; // 所有元素乘以2}}
}
5. 性能优化版本
使用指针进行高性能处理(必要时)
public unsafe class HighPerformanceMatrix
{public static void ProcessWithSpan(double[,] matrix){// 大多数情况下 Span 足够快var matrixSpan = new MatrixSpan<double>(matrix);// 优化的内循环int rows = matrixSpan.Rows;int cols = matrixSpan.Cols;for (int i = 0; i < rows; i++){// 获取当前行的 Span 视图var rowSpan = matrixSpan.AsFlatSpan().Slice(i * cols, cols);// 对整行进行操作for (int j = 0; j < cols; j++){rowSpan[j] = ProcessElement(i, j, rowSpan[j]);}}}// 只有在绝对必要时才使用指针public static unsafe void ProcessWithPointer(double[,] matrix){fixed (double* ptr = &matrix[0, 0]){int rows = matrix.GetLength(0);int cols = matrix.GetLength(1);for (int i = 0; i < rows; i++){double* rowPtr = ptr + (i * cols);for (int j = 0; j < cols; j++){rowPtr[j] = ProcessElement(i, j, rowPtr[j]);}}}}private static double ProcessElement(int i, int j, double value){return value * Math.Exp(-0.01 * (i * i + j * j));}
}
6. 三维数组的处理
对于更高维度的数组,原理相同:
public ref struct TensorSpan<T> where T : unmanaged
{private readonly Span<T> _span;private readonly int _dim1, _dim2, _dim3;public TensorSpan(T[,,] array){_span = MemoryMarshal.CreateSpan(ref array[0, 0, 0], array.Length);_dim1 = array.GetLength(0);_dim2 = array.GetLength(1);_dim3 = array.GetLength(2);}public ref T this[int i, int j, int k]{get{int index = (i * _dim2 * _dim3) + (j * _dim3) + k;return ref _span[index];}}
}// 使用示例
public void Process3DArray()
{double[,,] tensor = new double[10, 20, 30];var tensorSpan = new TensorSpan<double>(tensor);tensorSpan[1, 2, 3] = 42.0;
}
7. 与固定语句的对比
传统 fixed 方式
unsafe void TraditionalFixed(double[,] matrix)
{int rows = matrix.GetLength(0);int cols = matrix.GetLength(1);fixed (double* ptr = &matrix[0, 0]){for (int i = 0; i < rows; i++){double* row = ptr + (i * cols);for (int j = 0; j < cols; j++){row[j] = i * j * 0.1;}}}
}
现代 Span 方式
void ModernSpanApproach(double[,] matrix)
{var span = new MatrixSpan<double>(matrix);for (int i = 0; i < span.Rows; i++){for (int j = 0; j < span.Cols; j++){span[i, j] = i * j * 0.1;}}
}
8. 重要注意事项
- 内存连续性:多维数组在内存中是连续的,这是使用这些技术的前提
- 边界检查:
MatrixSpan封装提供了边界检查,比指针更安全 - 性能:对于大多数场景,Span 的性能与指针相当
- 可读性:Span 代码更易读和维护
- 安全性:不需要
unsafe上下文(除了高性能指针版本)
总结
对于多维数组,推荐的使用策略是:
- 首选:使用
MatrixSpan这样的封装,提供安全的二维索引访问 - 次选:直接使用
MemoryMarshal.CreateSpan创建一维视图 - 最后选择:仅在性能关键路径使用指针 + fixed
这种方法既保持了类型安全,又提供了接近指针的性能,同时代码更清晰易懂。