ZetCode

C# Span

最后修改:2025 年 2 月 15 日

在本文中,我们将展示如何在 C# 中使用 Span 类型。Span 类型是一种内存高效的方式来处理连续的内存区域,例如数组、字符串和堆栈分配的内存。它可以避免不必要的内存分配并提高性能。

Span 类型对于需要在不创建额外副本的情况下操作数组或字符串切片的场景特别有用。

Span 类型是一个 ref struct,它提供了连续内存区域的类型安全和内存安全表示。它可以指向堆栈、堆,甚至非托管内存中的内存。与数组不同,Span 不会分配内存;它只是提供对现有内存的视图。

创建 Span

以下示例演示如何从数组创建 Span

Program.cs
int[] numbers = { 1, 2, 3, 4, 5 };

// Create a Span from an array
Span<int> span = numbers.AsSpan();

// Modify the Span
span[0] = 10;

// Print the original array
Console.WriteLine(string.Join(", ", numbers));

在此程序中,使用 AsSpan 方法从数组创建 Span。修改 Span 也会修改原始数组,因为 Span 提供了对数组内存的视图。

$ dotnet run
10, 2, 3, 4, 5

切片 Span

以下示例演示如何切片 Span 以处理其元素的子集。

Program.cs
int[] numbers = { 1, 2, 3, 4, 5 };

// Create a Span from an array
Span<int> span = numbers.AsSpan();

// Slice the Span to get a subset
Span<int> slice = span.Slice(1, 3);

// Modify the slice
slice[0] = 20;

// Print the original array
Console.WriteLine(string.Join(", ", numbers));

在此程序中,Slice 方法用于创建 Span,该 Span 表示原始数组的子集。修改切片也会修改原始数组。

$ dotnet run
1, 20, 3, 4, 5

Span 与字符串

以下示例演示如何将 Span 与字符串一起使用,以避免不必要的分配。

Program.cs
string text = "an old falcon";

// Create a Span from a string
ReadOnlySpan<char> span = text.AsSpan();

// Slice the Span to get a substring
ReadOnlySpan<char> slice = span.Slice(7, 6);

// Print the slice
Console.WriteLine(slice.ToString());

在此程序中,使用 AsSpan 方法从字符串创建 ReadOnlySpanSlice 方法用于提取子字符串,而无需分配额外的内存。

$ dotnet run
falcon

使用 Span 进行堆栈分配

以下示例演示如何将 Span 与堆栈分配的内存一起使用。

Program.cs
// Allocate memory on the stack
Span<int> span = stackalloc int[5] { 1, 2, 3, 4, 5 };

// Modify the Span
span[0] = 10;

Console.WriteLine(string.Join(", ", span.ToArray()));

在此程序中,stackalloc 关键字用于在堆栈上分配内存,并创建一个 Span 来处理此内存。这避免了堆分配并提高了性能。

$ dotnet run
10, 2, 3, 4, 5

比较 Span 与数组的效率

以下示例演示了与传统数组相比,Span 的效率。 我们测量了使用这两种方法对一个大数组中的元素求和所花费的时间。

Program.cs
using System.Diagnostics;

int dataSize = 100_000_000;
int[] dataArray = new int[dataSize];
Random random = new();
for (int i = 0; i < dataSize; i++)
{
    dataArray[i] = random.Next(1, 100);
}

List<int> dataList = [.. dataArray];

void MeasureListSum()
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    long sum = 0;

    for (int i = 0; i < dataList.Count; i++)
    {
        sum += dataList[i];
    }

    stopwatch.Stop();
    Console.WriteLine($"List Sum: {sum}, Time: {stopwatch.ElapsedMilliseconds} ms");
}

void MeasureSpanSum()
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    long sum = 0;
    Span<int> dataSpan = dataArray;

    for (int i = 0; i < dataSpan.Length; i++)
    {
        sum += dataSpan[i];
    }

    stopwatch.Stop();
    Console.WriteLine($"Span Sum: {sum}, Time: {stopwatch.ElapsedMilliseconds} ms");
}

MeasureListSum();
MeasureSpanSum();

在此程序中,我们创建了一个包含 1 亿个整数的大数组,并测量了使用 ListSpan 对其元素求和所花费的时间。

int dataSize = 100_000_000;
int[] dataArray = new int[dataSize];
Random random = new();
for (int i = 0; i < dataSize; i++)
{
    dataArray[i] = random.Next(1, 100);
}

List<int> dataList = [.. dataArray];

创建一个大小为 100_000_000 的数组,并用随机整数填充。 然后使用该数组初始化 List<int>

Span<int> dataSpan = dataArray;

for (int i = 0; i < dataSpan.Length; i++)
{
    sum += dataSpan[i];
}

由于 Span<int> 具有较低的开销,并且能够直接处理数组切片而无需额外的内存分配,因此与 List<int> 相比,它可以提供性能优势。

$ dotnet run
List Sum: 5000008283, Time: 524 ms
Span Sum: 5000008283, Time: 412 ms

结果表明,使用 Span 比使用传统数组稍快,因为它避免了不必要的开销,并提供了一种更有效的方式来处理连续内存。

来源

C# Span - 文档

在本文中,我们展示了如何在 C# 中使用 Span 类型进行内存高效的操作。 Span 类型是一种强大的工具,可用于处理连续内存区域,而无需不必要的分配。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。到目前为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程