.NET 中 Span 类型和 Memory 类型的深度剖析
在 .NET 编程的世界里,高效处理内存是提升程序性能的关键。Span<T>
和 Memory<T>
类型的出现,为开发者提供了强大而灵活的工具,用于高效地访问和操作连续内存区域。今天,我们就来深入探讨这两种类型及其相关概念。
核心类型介绍
1. Span<T>
Span<T>
代表一块连续的、不可变长度的内存区域,可直接读写其中的元素。它可以在栈上声明,也能指向堆上分配的数据或其他内存位置。这种设计使得在不复制数据的情况下,能高效处理内存区域,尤其适用于处理大型数据结构、高性能计算以及与操作系统交互的场景。
2. ReadOnlySpan<T>
ReadOnlySpan<T>
是 Span<T>
的只读版本,提供对连续内存区域的只读访问。当不需要修改内存内容时,使用它能确保数据的安全性,同时享受与 Span<T>
类似的性能优势。
3. Memory<T>
和 ReadOnlyMemory<T>
System.Memory<T>
和 System.ReadOnlyMemory<T>
类似于 Span<T>
,但它们增加了表示更复杂内存所有权的能力。可以引用来自非托管内存、网络流、管道或内存映射文件等来源的数据。此外,它们支持跨线程和异步操作,可通过 IMemoryOwner<T>
接口表示所有权。
4. MemoryManager<T>
用于创建和管理 Memory<T>
和 ReadOnlyMemory<T>
实例。开发者可通过实现 MemoryManager<T>
自定义内存的分配和释放方式。
5. IMemoryOwner<T>
作为 MemoryManager<T>
的一个更简单的实现,表示临时拥有一段可释放内存的实体。可通过 MemoryPool<T>
获取此类实例,当内存不再需要时,可通过 Dispose()
方法释放资源。
6. MemoryPool<T>
内存池类,用于高效地分配和回收小块内存,避免频繁的内存分配和垃圾回收带来的开销。可以从内存池中租借 Memory<T>
实例,完成后归还给池。
7. Memory<T>.Pin()
方法和 GCHandle
当需要将 Span<T>
或 Memory<T>
转换为不受垃圾回收影响、可暴露给非托管代码的指针时,可使用 .Pin()
方法获取 MemoryHandle
,或直接使用 GCHandle
类来固定内存,常用于与非托管代码(如 C/C++ 互操作)的内存共享。
代码示例
Span<T>
和 ReadOnlySpan<T>
示例
// 创建一个整数数组
int[] array = { 1, 2, 3, 4, 5 };// 从数组创建 Span<T>
Span<int> span = new Span<int>(array);// 访问 Span 中的元素
Console.WriteLine(span[0]); // 输出: 1
span[0] = 10; // 修改 Span 中的元素会影响原始数组// 使用 Slice 方法获取 Span 的子集
Span<int> subSpan = span.Slice(1, 3); // 从索引 1 开始,长度为 3 的子集: { 10, 2, 3 }
foreach (var item in subSpan)
{Console.WriteLine(item);
}// 创建 ReadOnlySpan<T>
ReadOnlySpan<int> readOnlySpan = span.AsReadOnly();
Console.WriteLine(readOnlySpan[0]); // 输出: 10
// readOnlySpan[0] = 20; // 这会编译错误,因为 ReadOnlySpan<T> 是只读的
从这个示例可以看出,Span<T>
就像是对数组内存的一个“视图”,修改 Span<T>
中的元素会直接影响原始数组。而 ReadOnlySpan<T>
则提供了只读访问,保证了数据的安全性。
Memory<T>
和 ReadOnlyMemory<T>
示例
// 创建一个整数数组
int[] array = { 1, 2, 3, 4, 5 };// 从数组创建 Memory<T>
Memory<int> memory = MemoryMarshal.CreateFromArray(array);// 访问 Memory 中的元素(通过 Span 属性)
Span<int> spanFromMemory = memory.Span;
Console.WriteLine(spanFromMemory[0]); // 输出: 1
spanFromMemory[0] = 100; // 修改 Span 中的元素会影响原始数组和 Memory// 创建 ReadOnlyMemory<T>
ReadOnlyMemory<int> readOnlyMemory = memory;
ReadOnlySpan<int> readOnlySpan = readOnlyMemory.Span;
Console.WriteLine(readOnlySpan[0]); // 输出: 100
// readOnlySpan[0] = 200; // 这会编译错误,因为 ReadOnlySpan<T> 是只读的// 使用 Memory 的 Slice 方法
Memory<int> subMemory = memory.Slice(1, 3); // { 100, 2, 3 }
Span<int> subSpan = subMemory.Span;
foreach (var item in subSpan)
{Console.WriteLine(item);
}
Memory<T>
同样可以方便地访问和操作内存,但它更适合处理复杂的内存场景,尤其是涉及异步和跨线程操作。
Span<T>.Clear
方法示例
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = new Span<int>(array);
span.Clear();
Console.WriteLine(string.Join(", ", array)); // 输出 0, 0, 0, 0, 0
Span<T>.Clear
方法能快速将 Span<T>
中的所有元素设置为默认值,这在需要清空内存数据时非常实用。
MemoryMarshal
类示例
byte[] byteArray = { 1, 2, 3 };
Span<int> intSpan = MemoryMarshal.Cast<byte, int>(byteArray.AsSpan()).Slice(0, byteArray.Length / sizeof(int));
Console.WriteLine(intSpan[0]); // 输出 67305985,这是 1, 2, 3 的字节表示转换为 int 的结果
MemoryMarshal
类提供了一组静态方法,用于在 Span<T>
、Memory<T>
和其他类型之间进行转换和操作,大大增强了内存操作的灵活性。
Span<T>
与字符串示例
string str = "Hello";
ReadOnlySpan<char> charSpan = str.AsSpan();
Console.WriteLine(charSpan[0]); // 输出 Hchar[] charArray = new char[5];
Span<char> writableCharSpan = charArray.AsSpan();
writableCharSpan.Fill('A');
string newStr = new string(writableCharSpan);
Console.WriteLine(newStr); // 输出 AAAAA
通过 MemoryMarshal
或 Text.Encoding
类,可以在 Span<char>
和字符串之间进行转换,方便处理字符串相关的内存操作。
异步编程和跨线程操作
Memory<T>
和 ReadOnlyMemory<T>
非常适合用于异步编程和跨线程操作,因为它们是引用类型,可以安全地跨越方法边界和线程边界。而 Span<T>
是值类型,生命周期和范围受到限制,不适合直接用于这些场景。
以下是一个使用 Memory<T>
和 ReadOnlyMemory<T>
进行异步编程和跨线程操作的示例代码:
class Program
{static async Task Main(){// 创建一个整数数组int[] array = { 1, 2, 3, 4, 5 };// 从数组创建 Memory<T>Memory<int> memory = array.AsMemory();// 异步方法,接受 ReadOnlyMemory<T> 作为参数await ProcessMemoryAsync(memory);// 跨线程操作Task threadTask = Task.Run(() => ProcessMemoryOnThread(memory));await threadTask;}static async Task ProcessMemoryAsync(ReadOnlyMemory<int> readOnlyMemory){// 使用 ReadOnlySpan 来处理内存区域ReadOnlySpan<int> span = readOnlyMemory.Span;foreach (var item in span){// 模拟异步操作,例如 I/O 操作await Task.Delay(100);Console.WriteLine(item);}}static void ProcessMemoryOnThread(ReadOnlyMemory<int> readOnlyMemory){// 使用 ReadOnlySpan 来处理内存区域ReadOnlySpan<int> span = readOnlyMemory.Span;// 跨线程操作,这里只是简单地遍历并打印值foreach (var item in span){Console.WriteLine(item);}}
}
在这个示例中,Memory<T>
和 ReadOnlyMemory<T>
确保了在异步和跨线程操作中能够安全地共享对连续内存区域的引用,而无需复制数据,提高了性能。
总结
Span<T>
和 Memory<T>
类型为 .NET 开发者提供了强大的内存处理能力。Span<T>
适用于需要直接、高效访问内存的场景,而 Memory<T>
则更适合处理复杂的内存所有权和跨线程、异步操作。合理运用这些类型,可以显著提升程序的性能和资源利用率。在实际开发中,我们应根据具体的需求和场景,灵活选择使用这些类型,以达到最佳的编程效果。 ======================================================================
前些天发现了一个比较好玩的人工智能学习网站,通俗易懂,风趣幽默,可以了解了解AI基础知识,人工智能教程,不是一堆数学公式和算法的那种,用各种举例子来学习,读起来比较轻松,有兴趣可以看一下。
人工智能教程