ZetCode

C# ReadOnlySpan

最后修改日期:2025 年 4 月 27 日

本教程讲解如何在 C# 中使用 ReadOnlySpan 结构体来高效地处理内存。ReadOnlySpan 提供了一种安全的方式来处理内存切片而无需分配内存。

ReadOnlySpan 结构体表示一个连续的内存区域,是 .NET 中的一个关键特性,它允许高效地处理数据而无需创建不必要的副本。通过提供数组、字符串或原始内存的轻量级、不可变视图,ReadOnlySpan 优化了性能并降低了内存开销。它的设计确保任何修改都被禁止,这使其成为数据完整性和不变性至关重要的场景中的绝佳工具。此外,ReadOnlySpan 是在栈上分配的,有助于降低垃圾回收的压力并提高整体应用程序性能。

ReadOnlySpan 特别适合解析结构化数据、处理子字符串或处理数据缓冲区等任务,尤其是在高性能应用程序中。它的类型安全性确保开发人员可以自信地使用强类型数据,而其内置的边界检查可防止越界访问,从而降低运行时错误的概率。这些特性使 ReadOnlySpan 成为处理内存密集型操作中连续数据的强大抽象,在这些操作中,可靠性和效率都至关重要。

基本的 ReadOnlySpan 示例

此示例展示了如何从数组创建 ReadOnlySpan 并访问其元素而无需复制数据。

Program.cs
int[] numbers = { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> span = numbers.AsReadOnlySpan();

Console.WriteLine($"First: {span[0]}");
Console.WriteLine($"Length: {span.Length}");
Console.WriteLine($"Slice (2-4): {string.Join(", ", span.Slice(2, 2).ToArray())}");

该程序从一个整数数组创建一个 ReadOnlySpan,并演示了如何访问元素和切片。AsReadOnlySpan 创建一个跨度,而不复制数组。该示例使用索引访问第一个元素,检索跨度的长度,并创建一个从索引 2 开始的包含两个元素的切片。

由于 ReadOnlySpan 是不可变的,因此它可以确保底层数据无法被修改,从而在避免内存分配的同时保持性能。

使用 ReadOnlySpan 切割字符串

ReadOnlySpan 对于高效的字符串操作非常有用。此示例解析一个字符串,而不创建子字符串。

Program.cs
string text = "Hello, World!";
ReadOnlySpan<char> span = text.AsSpan();

ReadOnlySpan<char> hello = span.Slice(0, 5);
ReadOnlySpan<char> world = span.Slice(7, 5);

Console.WriteLine($"First word: {hello.ToString()}");
Console.WriteLine($"Second word: {world.ToString()}");

该示例从一个字符串创建一个 ReadOnlySpan,并使用切片提取子字符串。AsSpan 在字符串的字符上创建一个跨度,而不复制字符串。该程序通过指定起始索引和长度来切割跨度以提取“Hello”和“World”。

与传统的子字符串方法不同,使用 ReadOnlySpan 进行切片可以避免分配新字符串,从而提高性能。ToString 方法将跨度转换回字符串以进行显示,但切片本身是无分配的。

使用 ReadOnlySpan 解析数字

ReadOnlySpan 可以高效地解析数据。此示例从逗号分隔的字符串中解析整数。

Program.cs
using System;

string data = "42,100,7";
ReadOnlySpan<char> span = data.AsSpan();
List<int> numbers = [];

while (!span.IsEmpty)
{
    int commaIndex = span.IndexOf(',');
    ReadOnlySpan<char> numberSpan = commaIndex == -1 ? span : span.Slice(0, commaIndex);
    numbers.Add(int.Parse(numberSpan));
    span = commaIndex == -1 ? [] : span.Slice(commaIndex + 1);
}

Console.WriteLine($"Parsed numbers: {string.Join(", ", numbers)}");

该程序使用 ReadOnlySpan 从字符串中解析整数,以避免分配内存。它从输入字符串创建一个跨度,并迭代查找逗号以分割数据。对于每个段,IndexOf 找到下一个逗号,Slice 提取数字部分。int.Parse 将跨度转换为整数,并将其添加到列表中。

跨度被更新以跳过逗号并继续解析。这种方法是高效的,因为它避免了创建中间字符串,而是依赖于 ReadOnlySpan 直接使用字符串内存的能力。

使用内存缓冲区

ReadOnlySpan 可以处理原始内存缓冲区。此示例将字节缓冲区作为跨度进行处理。

Program.cs
byte[] buffer = { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 };
ReadOnlySpan span = buffer;

int firstInt = BitConverter.ToInt32(span.Slice(0, 4));
int secondInt = BitConverter.ToInt32(span.Slice(4, 4));

Console.WriteLine($"First int: {firstInt}");
Console.WriteLine($"Second int: {secondInt}");

该示例将字节数组视为 ReadOnlySpan 以从缓冲区读取整数。该数组包含两个小端格式的 32 位整数。Slice 提取 4 字节段,BitConverter.ToInt32 将每个段转换为整数。使用 ReadOnlySpan 避免了复制缓冲区,并提供了一种安全、边界检查的方式来访问数据。此技术对于处理二进制数据(例如网络数据包或文件格式)非常有用,在这些数据中,需要直接内存访问而无需分配内存。

将 ReadOnlySpan 与栈分配一起使用

ReadOnlySpan 可以与栈分配的内存一起使用,以实现最大效率。此示例使用 stackalloc。

Program.cs
using System;

Span<int> temp = stackalloc int[3];
temp[0] = 10; temp[1] = 20; temp[2] = 30;
ReadOnlySpan<int> span = temp;

Console.WriteLine($"Sum: {span[0] + span[1] + span[2]}");
Console.WriteLine($"Slice (1-2): {string.Join(", ", span.Slice(1, 2).ToArray())}");

该程序使用 stackalloc 在栈上分配一个临时数组,该数组被转换为 ReadOnlySpan。 Span<int> 填充了值,然后转换为 ReadOnlySpan 以进行安全、只读访问。该示例计算元素的总和并创建一个切片。栈分配避免了堆分配,使这种方法对于小型、短期的缓冲区非常有效。

ReadOnlySpan 确保通过边界检查安全地访问数据,并且其不变性可防止对底层内存的意外修改。

使用 ReadOnlySpan 比较字符串

ReadOnlySpan 支持高效的字符串比较。此示例比较字符串的部分内容。

Program.cs
using System;

string text1 = "Hello, World!";
string text2 = "Hello, Universe!";
ReadOnlySpan<char> span1 = text1.AsSpan(0, 5);
ReadOnlySpan<char> span2 = text2.AsSpan(0, 5);

bool areEqual = span1.SequenceEqual(span2);
Console.WriteLine($"First 5 chars equal: {areEqual}");

该示例使用 ReadOnlySpan 比较两个字符串的前五个字符。AsSpan 在字符串的相关部分上创建跨度,SequenceEqual 检查它们是否相同。

此方法比创建子字符串或使用传统的字符串比较方法更有效,因为它直接在字符串的内存上操作而无需分配内存。 此技术在性能关键型场景(例如解析或验证大型数据集)中特别有用,在这些场景中,最大限度地减少内存使用和处理时间至关重要。

带有 ReadOnlyMemory 的 ReadOnlySpan

ReadOnlySpan 可以与 ReadOnlyMemory 一起使用以实现灵活的内存管理。此示例演示了转换。

Program.cs
using System;

int[] numbers = { 1, 2, 3, 4, 5 };
ReadOnlyMemory<int> memory = numbers.AsMemory();
ReadOnlySpan<int> span = memory.Span;

Console.WriteLine($"First: {span[0]}");
Console.WriteLine($"Last: {span[^1]}");

该程序将数组转换为 ReadOnlyMemory,然后从中提取 ReadOnlySpan。 AsMemory 创建一个内存对象,并且 Span 属性提供了一个跨度以进行直接访问。该示例使用索引访问第一个和最后一个元素。 ReadOnlyMemory 对于在方法或异步操作之间传递内存区域非常有用,而 ReadOnlySpan 提供了一个轻量级的、基于栈的视图以进行立即处理。 这种组合允许灵活而高效的内存管理,尤其是在涉及异步或分层数据处理的场景中。

来源

ReadOnlySpan 结构体文档

本教程介绍了如何在 C# 中使用 ReadOnlySpan 进行高效的内存处理,包括数组、字符串、缓冲区和栈分配的内存。

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 我自 2007 年以来一直在撰写编程文章。 至今,我已撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程