C#中的 Span、fixed、多维数组

在C#中,多维数组(如double[,])与交错数组(double[][])不同。

多维数组是矩形数组,在内存中是连续存储的。

但是,Span最初是为连续内存区域设计的,因此对于多维数组,我们可以使用MemoryMarshal来创建一个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主要用于一维数据,我们可以将二维数组视为一维数组来创建一个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:将多维数组视为一维 Span
        Span<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;
    }
    
    // 通过二维索引访问 Span
    public 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];
        
        // 创建 MatrixSpan
        var 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. 重要注意事项

  1. 内存连续性:多维数组在内存中是连续的,这是使用这些技术的前提
  2. 边界检查MatrixSpan 封装提供了边界检查,比指针更安全
  3. 性能:对于大多数场景,Span 的性能与指针相当
  4. 可读性:Span 代码更易读和维护
  5. 安全性:不需要 unsafe 上下文(除了高性能指针版本)

总结

对于多维数组,推荐的使用策略是:

  1. 首选:使用 MatrixSpan 这样的封装,提供安全的二维索引访问
  2. 次选:直接使用 MemoryMarshal.CreateSpan 创建一维视图
  3. 最后选择:仅在性能关键路径使用指针 + fixed

这种方法既保持了类型安全,又提供了接近指针的性能,同时代码更清晰易懂。

posted @ 2025-10-27 13:22  青云Zeo  阅读(10)  评论(0)    收藏  举报