CodeGym /课程 /C# SELF /Memory<T> 和 ReadOnlyMemory<T>

Memory<T> 和 ReadOnlyMemory<T>

C# SELF
第 65 级, 课程 4
可用

1. 介绍

一切开始于 .NET 的开发者想提升大数据量处理的速度,并且尽可能给开发者既方便又安全的工具。最先出现的是 Span<T>,它是对连续内存区域的“窗口”,可以指向数组、字符串,甚至是 .NET 之外分配的内存(比如通过 P/Invoke)。

不过 Span<T> 有一个强限制:它必须只活在栈上。不能把它存在类字段里,不能从方法返回,不能在异步方法之间传递。原因是安全:如果有人保留了对已经不存在内存的引用,应用就会崩溃。

有时候你需要从方法返回内存切片、把它放到集合或类字段里,或者在异步 API 中使用。这里就轮到 Memory<T> 上场——它本质上是 Span<T> 的“长期存在”版本,可以放到堆上、在线程间传递、作为属性和对象的一部分,行为像一个正常的 .NET 对象。

还有一个“只读大哥”—— ReadOnlyMemory<T>,顾名思义,它不允许通过这个视图修改底层数据,但可以在任何需要读取的地方使用。

2. Span<T> 和 Memory<T> 的区别

下面是个简单表格,方便对比:

Span<T>
Memory<T>
存放位置 只在栈上 (stack only) 可以在栈上也可以在堆上 (heap/stack)
可以存储在字段中 ❌ 不可以 ✅ 可以
可以从方法返回 ❌ 不可以 ✅ 可以
异步 / await 方法 ❌ 不行 ✅ 可以
可变性 ✅ 有对应的 ReadOnlySpan<T> ✅ 有对应的 ReadOnlyMemory<T>
支持切片 ✅ 支持 ✅ 支持

如果你只需要在方法内部快速遍历数据——用 Span<T>。如果你需要把结果返回到外部或放到类字段里——用 Memory<T>。如果数据只是读的——用 ReadOnlyMemory<T>。

3. Memory<T> 的签名和基本结构

和常规一样:Memory<T> 是泛型类型。你可以创建 Memory<int>、Memory<byte>、Memory<char>,甚至 Memory<MyType>。在 Memory<T> 内部保存着对数组、字符串或其他数据源的引用,以及一个范围(从哪个索引开始和有多少元素)。

如果想从 Memory<T> 获取快速处理能力,可以用它的属性 Span —— 这样你会立刻得到一个 Span<T>,可以在同步方法里使用。

4. 怎么创建 Memory<T>:实践

示例 1. 从数组创建

int[] numbers = { 1, 2, 3, 4, 5, 6 };
Memory<int> memory = new Memory<int>(numbers); // 整个数组

// 可以取一个 "切片" —— 数组的一部分
Memory<int> slice = memory.Slice(2, 3); // 元素 2、3 和 4

示例 2. 从字符串创建(通过 Memory<char>)

string text = "你好,世界!";
Memory<char> charMemory = text.AsMemory(); // 整个文本作为内存
Memory<char> subMemory = charMemory.Slice(7, 3); // 从第7个字符开始,3个字符("世界")

示例 3. 使用 ReadOnlyMemory<T>

用法完全一样,只是不能通过它修改数据:

int[] data = { 10, 20, 30, 40 };
ReadOnlyMemory<int> readOnly = data; // 通过这个对象不能修改底层数据

5. 在 Memory<T> 和 Span<T> 之间转换

直接用 Memory<T> 达到 Span<T> 那样的灵活和速度是不行的——它们的设计目标不同。不过当你真的需要快速处理一段内存时,可以通过属性 Span 从 memory 中获得一个即时的 Span<T>:

void ProcessData(Memory<int> memory)
{
    Span<int> span = memory.Span;
    for (int i = 0; i < span.Length; i++)
    {
        span[i] += 100;
    }
}

注意:Span<T> 只在方法内部有效。如果尝试把它返回出去,编译器会报错。

对 ReadOnlyMemory<T> 也是类似的,你会得到一个 ReadOnlySpan<T>,它不允许你修改数据:

void PrintData(ReadOnlyMemory<int> memory)
{
    ReadOnlySpan<int> roSpan = memory.Span;
    foreach (var item in roSpan)
        Console.WriteLine(item);
}

6. 在真实场景中的使用

异步处理数据

这正是 Memory<T> 发光的地方。它可以用于异步方法!比如异步读文件:

using System.IO;
using System.Threading.Tasks;

public async Task ReadFileAsync(string path)
{
    byte[] buffer = new byte[4096];
    using var stream = File.OpenRead(path);
    int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
    // 现在你可以处理 buffer 了
}

这里 AsMemory 把缓冲区直接传给了异步方法,不会出现像用 Span<T> 那样的作用域或内存销毁问题。

在属性和字段中存储切片

有时候你想做个类,存一个大数组的“片段”以便稍后传递:

class DataChunk
{
    public Memory<byte> Data { get; }

    public DataChunk(Memory<byte> data)
    {
        Data = data;
    }
}

7. 和集合、字符串、数组一起用

和数组

最常见的用法:

byte[] bytes = { 1, 2, 3, 4, 5 };
Memory<byte> mem = bytes;          // 整个数组
Memory<byte> part = mem.Slice(2);  // 从第3个元素到末尾

和字符串

通过 AsMemory():

string hello = "Hello, Memory!";
ReadOnlyMemory<char> mem = hello.AsMemory(6, 6); // "Memory"

和集合(例如 List<T>)

不能直接从 List<T> 创建 Memory<T>。只能先转成数组:

List<int> list = new List<int> { 1, 2, 3 };
Memory<int> mem = list.ToArray(); // 这是一次复制,不是引用!

注意:如果想避免复制——把数据保存在数组中。

8. 使用 Memory<T> 的常见错误

错误 №1:试图在类字段中用 Span<T> 替代 Memory<T>。 不能把 Span<T> 存在类字段里,因为它绑定到栈。编译器会报错。要在堆上保存,就用 Memory<T>。

错误 №2:认为切片会复制数据。 Memory<T> 不会复制数据,它只是创建了一个指向现有数组的“窗口”。如果你通过一个 Memory<T> 修改数据,所有引用同一块内存的视图都会看到变化。

错误 №3:试图直接从 List<T> 创建 Memory<T>。 Memory<T> 只能和数组一起工作,因为 List<T> 可能会改变底层数据在内存中的位置。用 ToArray() 把列表转成数组。

错误 №4:忽视 ReadOnlyMemory<T> 在不可变数据上的价值。 如果数据不需要修改,用 ReadOnlyMemory<T> 替代 Memory<T> 会更安全。

2
任务
C# SELF, 第 65 级, 课程 4
已锁定
使用 Memory 进行异步操作
使用 Memory 进行异步操作
1
调查/小测验
C#里的内存第 65 级,课程 4
不可用
C#里的内存
.NET里的内存结构
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION