最后更新时间:2026-01-18 14:29:29
本文通过豆包辅助生成!
在 C# 中,ReadOnlySpan<T> 是处理内存片段的核心类型,尤其适合高频读写、低内存开销的场景。本文将从“是什么、怎么用、为什么好用”三个维度,带你快速掌握 ReadOnlySpan<T> 的基础用法和核心优势。
一、ReadOnlySpan<T> 是什么?
ReadOnlySpan<T> 是 .NET Core 2.1+/.NET 5+ 引入的只读内存跨度类型,本质是对一段连续内存(如字符串、数组、非托管内存)的“轻量视图”——它不分配新内存,仅记录内存的起始地址和长度,且无法修改指向的内存内容,因此兼具高性能和内存安全。
核心特点:
- 栈分配(stack-only):实例存储在栈上,无 GC 开销;
- 只读特性:仅能读取内存内容,无法修改,避免意外数据篡改;
- 零拷贝:操作内存时不复制数据,直接引用原内存区域。
注意:ReadOnlySpan<T> 不能用于异步方法的返回值、类的字段等场景(栈类型限制),若需跨上下文使用,可改用 ReadOnlyMemory<T>。
二、如何将常见类型转为 ReadOnlySpan<T>?
ReadOnlySpan<T> 无法直接创建(无公共构造函数),需通过 MemoryExtensions.AsSpan 方法将字符串、数组、列表等转为 ReadOnlySpan<T>。
前置条件:安装依赖(低版本 .NET 需处理)
若你的 .NET 版本(如 .NET Framework 4.x)未内置 MemoryExtensions,需先安装 NuGet 包:
1 | Install-Package System.Memory |
1. 字符串转 ReadOnlySpan<char>
字符串本质是 char 数组,转换后可直接操作字符片段,且无字符串拷贝开销:
1 | // 原始字符串 |
2. 数组转 ReadOnlySpan<T>
任意数组(如 int[]、byte[])均可直接转换,适配任意值类型/引用类型:
1 | // 原始 int 数组 |
3. List<T> 转 ReadOnlySpan<T>
若要将 List<T> 转为 ReadOnlySpan<T>,有两种方法可以实现:
- 先将 List<T> 通过 ToArray() 转为数组(因 List 内存非绝对连续),再转 ReadOnlySpan<T>:
1 | // 原始 List<string> |
- 通过 CollectionsMarshal.AsSpan<T>(List<T>) 转为 ReadOnlySpan<T>:
1 | // 原始 List<string> |
CollectionsMarshal 类需要 .NET 5+ 才能使用,或者通过 NuGet 安装包 System.Runtime.InteropServices(未测试过,不确定存不存在 CollectionsMarshal 类)。
三、为什么要使用 ReadOnlySpan<T>?核心优势
相比直接操作字符串、数组,ReadOnlySpan<T> 最大的价值是零拷贝 + 低 GC 开销,具体优势如下:
1. 核心优势总结
| 优势 | 说明 |
|---|---|
| 零内存拷贝 | 操作内存片段时(如截取子串、取数组片段),仅修改“视图范围”,不复制原数据 |
| 无 GC 压力 | 栈分配类型,实例无需 GC 回收;避免频繁创建临时字符串/数组导致的 GC 触发 |
| 内存安全 | 只读特性防止意外修改原数据,索引越界会直接抛出异常,避免内存越访问 |
| 高性能 | 直接操作内存地址,比传统字符串/数组方法(如 string.Substring)快数倍 |
2. 对比 string:效率大幅提升
传统 string 是不可变类型,调用 Substring、Split 等方法时,会创建新字符串实例(拷贝原字符数据),高频操作时会产生大量临时对象,触发频繁 GC。
而 ReadOnlySpan<char> 操作字符串时无拷贝:
1 | // 传统方式:创建新字符串(拷贝数据) |
补充:高版本 .NET 的 string 内置方法(如 Substring)已底层适配 ReadOnlySpan<char>,但低版本 .NET 仍为拷贝实现——因此低版本中手动用 ReadOnlySpan<char> 优化效果更显著。
3. 对比数组/列表:更轻量的内存操作
直接操作数组时,截取片段(如 Array.Copy)需拷贝数据;而 ReadOnlySpan<T> 仅通过 Slice 方法调整视图,无拷贝开销:
1 | int[] nums = { 1, 2, 3, 4, 5 }; |
四、ReadOnlySpan<T> 核心属性(案例:string)
仅介绍高频使用的 2 个核心属性,案例基于字符串场景:
1. Item[Int32]:获取指定索引的元素
通过索引访问 ReadOnlySpan<T> 中的元素,语法与数组一致,只读不可改:
1 | string text = "C# ReadOnlySpan Tutorial"; |
2. Length:获取只读范围的元素数量
返回 ReadOnlySpan<T> 包含的元素总数,等价于原数据的有效长度:
1 | string text = "Hello ReadOnlySpan"; |
五、ReadOnlySpan<T> 核心方法(案例:数组)
以下方法均基于 int[] 数组案例,聚焦高频使用的 4 个方法:
1. CopyTo(Span<T>):复制内容到目标 Span<T>
将 ReadOnlySpan<T> 的内容复制到可写的 Span<T>(需保证目标 Span 长度足够,否则抛异常):
1 | // 源数组 → ReadOnlySpan<int> |
2. TryCopyTo(Span<T>):安全复制(返回操作结果)
与 CopyTo 功能一致,但不会抛异常——若目标 Span 长度不足,返回 false,否则返回 true:
1 | int[] source = { 10, 20, 30, 40 }; |
3. Slice(Int32):从指定索引开始截取片段
截取从 startIndex 到末尾的所有元素,零拷贝仅调整视图:
1 | int[] nums = { 1, 2, 3, 4, 5 }; |
4. Slice(Int32, Int32):指定索引+长度截取片段
截取从 startIndex 开始、长度为 length 的片段,需保证 startIndex + length ≤ 原 Span 长度:
1 | int[] nums = { 1, 2, 3, 4, 5 }; |
补充:其他常用方法
ReadOnlySpan<T> 还内置了大量实用方法,可参考官方文档/网络教程:
- Contains(T):判断是否包含指定元素;
- StartsWith(ReadOnlySpan<T>):判断是否以指定片段开头;
- EndsWith(ReadOnlySpan<T>):判断是否以指定片段结尾;
- IndexOf(T):查找指定元素的第一个索引;
- LastIndexOf(T):查找指定元素的最后一个索引。
总结
- ReadOnlySpan<T> 是只读的内存视图,通过 AsSpan() 从字符串/数组/列表转换,低版本需安装 System.Memory;
- 核心优势是零拷贝、低 GC 开销,相比传统字符串/数组操作效率大幅提升;
- 核心属性:Item[Int32](索引访问)、Length(获取长度);
- 核心方法:CopyTo/TryCopyTo(复制)、Slice(截取片段),适配高性能内存操作场景。
ReadOnlySpan<T> 尤其适合高频处理字符串、字节数组的场景(如网络IO、数据解析),是 C# 高性能编程的必备工具。
外部链接
System.Memory - NuGet Gallery
System.Runtime.InteropServices - NuGet Gallery
ReadOnlySpan<T> 结构 - Microsoft Learn
MemoryExtensions 类 - Microsoft Learn
CollectionsMarshal 类 - Microsoft Learn