1. Giới thiệu
Mọi thứ bắt đầu từ việc các dev .NET muốn tăng tốc xử lý dữ liệu lớn và cho phép làm việc tiện và an toàn nhất có thể. Ban đầu có Span<T>, đó là "cửa sổ" vào vùng nhớ liên tục, có thể hiển thị một phần của array, string hoặc thậm chí bộ nhớ cấp phát ngoài .NET (ví dụ qua P/Invoke).
Tuy nhiên Span<T> có một giới hạn mạnh: nó luôn phải sống trên stack. Không thể lưu nó trong fields của class, không thể return từ method, không thể truyền giữa các phương thức async. Lý do là an toàn: nếu ai đó giữ tham chiếu tới vùng nhớ đã không còn tồn tại thì app chắc chắn crash.
Đôi khi cần return slices từ method, lưu chúng trong collection hoặc fields của class, và dùng trong API bất đồng bộ. Đây là lúc Memory<T> xuất hiện — về cơ bản là phiên bản "sống lâu" an toàn của Span<T>, có thể sống trên heap, truyền giữa threads, nằm trong properties và object và hành xử như một object .NET tử tế.
Còn có "anh lớn" nữa — ReadOnlyMemory<T>, như bạn đoán, không cho sửa dữ liệu bên dưới, nhưng cho phép đọc ở mọi nơi cần.
2. Khác nhau giữa Span<T> và Memory<T>
Đây là bảng nhỏ để so sánh trực quan:
|
|
|
|---|---|---|
| Nơi sống | Chỉ trên stack (stack only) | Trên stack và heap (heap/stack) |
| Có thể lưu vào field | ❌ Không | ✅ Có |
| Có thể return từ method | ❌ Không | ✅ Có |
| Bất đồng bộ/await-method | ❌ Không được | ✅ Được |
| Có thể chỉnh sửa | ✅ Có phiên bản ReadOnlySpan<T> | ✅ Có phiên bản ReadOnlyMemory<T> |
| Cho phép slice dữ liệu | ✅ Có | ✅ Có |
Nếu bạn cần lướt nhanh dữ liệu trong method — dùng Span<T>. Nếu cần trả kết quả ra ngoài hoặc lưu vào field của class — dùng Memory<T>. Còn nếu chỉ đọc — chọn ReadOnlyMemory<T>.
3. Chữ ký và cấu trúc cơ bản của Memory<T>
Như thường lệ: Memory<T> là generic. Có thể tạo Memory<int>, Memory<byte>, Memory<char> và thậm chí Memory<MyType>. Bên trong Memory<T> có một tham chiếu tới array, string hoặc nguồn dữ liệu khác, và thông tin về range (bắt đầu từ chỉ số nào và bao nhiêu phần tử).
Để có truy cập nhanh từ Memory<T> để xử lý, dùng property Span — lúc đó bạn sẽ có ngay Span<T> để dùng trong method đồng bộ.
4. Cách tạo Memory<T>: Thực hành
Ví dụ 1. Tạo từ array
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Memory<int> memory = new Memory<int>(numbers); // Toàn bộ array
// Có thể lấy "slice" — một phần của array
Memory<int> slice = memory.Slice(2, 3); // phần tử 2, 3 và 4
Ví dụ 2. Tạo từ string (qua Memory<char>)
string text = "Privet, mir!";
Memory<char> charMemory = text.AsMemory(); // Toàn bộ text dưới dạng memory
Memory<char> subMemory = charMemory.Slice(7, 3); // từ ký tự thứ 7, 3 ký tự ("mir")
Ví dụ 3. Dùng ReadOnlyMemory<T>
Giống hệt, chỉ khác là không cho sửa:
int[] data = { 10, 20, 30, 40 };
ReadOnlyMemory<int> readOnly = data; // Không cho sửa qua object này
5. Chuyển đổi giữa Memory<T> và Span<T>
Làm việc với Memory<T> không thể linh hoạt và nhanh như Span<T> trực tiếp — vì mục đích khác nhau. Nhưng khi cần xử lý nhanh một phần bộ nhớ, bạn có thể lấy Span<T> "tạm thời" từ memory qua property Span:
void ProcessData(Memory<int> memory)
{
Span<int> span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
span[i] += 100;
}
}
Lưu ý: Span<T> chỉ làm việc trong method. Nếu cố return nó ra ngoài, compiler sẽ báo lỗi.
Với ReadOnlyMemory<T> cũng tương tự, nhưng bạn nhận được ReadOnlySpan<T>, không cho phép thay đổi:
void PrintData(ReadOnlyMemory<int> memory)
{
ReadOnlySpan<int> roSpan = memory.Span;
foreach (var item in roSpan)
Console.WriteLine(item);
}
6. Ứng dụng trong bài toán thực tế
Xử lý bất đồng bộ dữ liệu
Đây là lúc Memory<T> tỏa sáng. Nó có thể dùng trong async methods! Ví dụ, đọc file bất đồng bộ:
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));
// Bây giờ bạn có thể làm việc với buffer
}
Ở đây AsMemory truyền buffer trực tiếp vào method async, và không có vấn đề scope hoặc bộ nhớ bị hủy như với Span<T>.
Lưu slice dữ liệu trong properties và fields
Đôi khi cần một class lưu "mẩu" của array lớn để sau gửi đi:
class DataChunk
{
public Memory<byte> Data { get; }
public DataChunk(Memory<byte> data)
{
Data = data;
}
}
7. Dùng với collection, string và array
Với array
Thường thì như sau:
byte[] bytes = { 1, 2, 3, 4, 5 };
Memory<byte> mem = bytes; // Toàn bộ array
Memory<byte> part = mem.Slice(2); // Từ phần tử thứ ba tới cuối
Với string
Qua AsMemory():
string hello = "Hello, Memory!";
ReadOnlyMemory<char> mem = hello.AsMemory(6, 6); // "Memory"
Với collection (ví dụ List<T>)
Không thể trực tiếp tạo Memory<T> từ List<T>. Chỉ có thể qua array:
List<int> list = new List<int> { 1, 2, 3 };
Memory<int> mem = list.ToArray(); // Sao chép, không phải tham chiếu!
Chú ý: nếu muốn tránh copy — giữ dữ liệu trong array.
8. Lỗi thường gặp khi làm việc với Memory<T>
Lỗi #1: cố dùng Span<T> thay vì Memory<T> trong field của class. Không thể lưu Span<T> trong field của class vì nó gắn với stack. Compiler sẽ báo lỗi. Dùng Memory<T> để lưu trên heap.
Lỗi #2: tưởng rằng slice sẽ copy dữ liệu. Memory<T> không copy dữ liệu, nó tạo "cửa sổ" vào array tồn tại. Nếu sửa dữ liệu qua một Memory<T>, thay đổi sẽ hiển thị ở tất cả các object khác tham chiếu cùng vùng nhớ.
Lỗi #3: cố tạo Memory<T> trực tiếp từ List<T>. Memory<T> chỉ làm việc với array, vì List<T> có thể di chuyển dữ liệu trong bộ nhớ. Chuyển list thành array bằng ToArray().
Lỗi #4: bỏ qua ReadOnlyMemory<T> cho dữ liệu không thay đổi. Nếu dữ liệu không cần sửa, dùng ReadOnlyMemory<T> thay vì Memory<T> để an toàn hơn.
GO TO FULL VERSION