解读 --- Span<T>

引言

Span<T> 是C# 中的一种结构体,它是一种内存安全的类型,可以用来表示连续的内存区域。Span<T> 可以被用于访问和操作数组、堆上分配的内存和栈上分配的内存。使用 Span<T> 可以避免不必要的内存拷贝,从而提高性能。

对数组使用Span

如果需要快速访问托管或非托管的连续内存,可以使用 Span<T>结构。Span<T> 结构表示存储连续的内存。所以使用它的数据结构一般也使用连续的内存。例如:

  • 数组
  • 长字符串(实际上也是数组)

使用 Span<T>,可以直接访问数组元素。且数组的元素不会复制,可以直接使用它们,这样比复制效率要高。例如下面的代码:

static void Main(string[] args)
{
    int[] source = new int[] { 1, 2, 3 };

    int[] arr = new int[] { source[0], source[1], source[2] };
    
    arr[0] = 33;
    
    Console.WriteLine($"The first element of source is {source[0]}");
    Console.WriteLine($"The first element of arr is {arr[0]}");

    Span<int> span = new(source);
    
    span[0] = 11;
    
    Console.WriteLine($"The first element of source is {source[0]}");
    Console.WriteLine($"The first element of span is {span[0]}");
    
    Console.ReadLine();
}

可以先猜测以下上述代码的输出是什么?

输出:

The first element of source is 1
The first element of arr is 33
The first element of source is 11
The first element of span is 11

上述代码段中,先声明了一个源数组 source 和一个数组 arr ,并将 source 的值复制给 arr 。然后修改 arr 中的第一个元素值为33,可以看到结果 arr 的第一个元素已经改变为33,source 保持不变。然后又声明了一个 Span<int> ,它引用 source 数组。因为Span<T>是直接访问数组元素,而不是复制元素,所以修改 span 中的第一个元素为11, source 中的第一个元素也被修改为11。

创建切片

Span<T> 的一个强大特性是,可以使用它访问数组的部分或切片。使用切片时,不会复制数组元素,它们是从span 中直接访问的。

有如下代码段:

static void Main(string[] args)
{
    int[] source = { 1, 6, 23, 76, 88, 213 };
    
    Span<int> span1 = new Span<int>(source, start: 1, length: 4);
    
    Span<int> span2 = span1.Slice(start: 1, length: 3);
    
    DisplaySpan("span1 contains the elements:", span1);
    
    DisplaySpan("span2 contains the elements:", span2);
    
    Console.ReadLine();
}

private static void DisplaySpan(string content, Span<int> span1)
{
    Console.Write(content);
    
    foreach (var item in span1)
    {
        Console.Write(item + ",");
    }
    
    Console.WriteLine();
}

下面的代码片段展示了创建切片的两种方法。

  1. 除默认构造函数传参数组之外,另一种重载是直接使用构造函数传递源数组,起始位置和长度。例如上述代码中 new Span<int>(source, start: 1, length: 4) 它表示在源数组中从第2个元素开始访问数组的4个元素。
  2. 直接从span中再次切片,传入起始位置和长度,例如上述代码中span1.Slice(start: 1, length: 3)表示从span1中第2个元素开始包含3个元素的切片。

输出:

span1 contains the elements:6,23,76,88,
span2 contains the elements:23,76,88,

这里使用时一定注意传入参数 startlength 后的越界问题。

使用Span改变值

在文章开头,介绍了如何使用 Span<T> 的索引器,直接更改由 span 直接引用的数组元素,实际上它还有其他改变值的方法。

例如:

  • Slice(int start, int length):返回一个新的 Span<T>,它表示从 Span<T> 的指定起始位置开始的指定长度部分。可以使用该方法来获取或更改 Span<T> 中的子集。

  • Clear():将 Span<T> 中的所有元素设置为默认值 default<T>

  • Fill(T value):将 Span<T> 中的所有元素设置为指定的值。

  • CopyTo(Span<T> destination):将 Span<T> 中的所有元素复制到指定的目标 Span<T>

  • CopyTo(T[] destination):将 Span<T> 中的所有元素复制到指定的目标数组。

  • Reverse():反转 Span<T> 中的元素顺序。

  • Sort():对 Span<T> 中的元素进行排序。

请注意,这些方法都是按值传递的,而不是按引用传递的。这意味着在调用这些方法时,将复制 Span<T> 中的值。如果您想要修改原始 Span<T> 中的值,请使用引用传递方式,例如使用 ref Span<T> 参数。

只读的Span

如果只需要对数组片段进行读访问,则可以使用 ReadOnlySpan<T>,可以使用它来读取内存块中的数据,而不必担心其他代码同时修改了该内存块。

对于 ReadOnlySpan<T> ,它的索引器是只读的,所以这种类型没有提供 ClearFill 方法,但是可以调用 CopyTo() 方法,将 ReadOnlySpan<T> 的内容复制到 Span<T>

此外,它支持隐式转换,由数组或 Span<T> 直接赋值给 ReadOnlySpan<T>,如下:

static void Main(string[] args)
{
    int[] source = { 1, 6, 23, 76, 88, 213 };

    Span<int> span = new Span<int>(source);

    DisplaySpan("span contains the elements:", span);

    ReadOnlySpan<int> readOnlySpan = source;

    DisplaySpan("readOnlySpan contains the elements:", readOnlySpan);

    Console.ReadLine();
}

private static void DisplaySpan(string content, ReadOnlySpan<int> span1)
{
    Console.Write(content);

    foreach (var item in span1)
    {
        Console.Write(item + ",");
    }

    Console.WriteLine();
}

输出:

span contains the elements:1,6,23,76,88,213,
readOnlySpan contains the elements:1,6,23,76,88,213,

Span<T> 相比,ReadOnlySpan<T> 的一个重要的限制是不允许修改其包含的内存块。这使得 ReadOnlySpan<T> 更适合于读取内存块中的数据,而不是修改它们。

posted @ 2023-08-07 08:17  NiueryDiary  阅读(2010)  评论(7编辑  收藏  举报