1. Giới thiệu
Hãy tưởng tượng bạn có một mảng lớn dữ liệu, ví dụ các byte đọc từ file. Bạn muốn xử lý một phần — đọc một đoạn thành chuỗi hoặc chỉ truyền một phần dữ liệu vào method. Theo cách "cũ" bạn sẽ copy các byte đó vào mảng mới — điều này chậm và tốn bộ nhớ. Nếu có hàng chục hay hàng trăm đoạn như vậy thì sao?
Và đây là lúc nhân vật chính xuất hiện — Span<T>. Đây là một struct đặc biệt mô tả một lát (view) lên mảng (hoặc bất kỳ khối nhớ liên tiếp nào), không copy dữ liệu mà chỉ trỏ tới vùng cần thiết. Quan trọng: Span<T> không phải collection mới, mà là một "cửa sổ" an toàn lên mảng hiện có!
Đặc tính chính và hạn chế của Span<T>
- Span<T> là struct (value type), đại diện cho "cửa sổ" lên bộ nhớ và không copy các phần tử.
- Chỉ tham chiếu tới vùng bộ nhớ liên tiếp: mảng, một phần mảng, block từ stackalloc, bộ nhớ chuỗi qua các method đặc biệt, hoặc bộ nhớ trong unsafe code.
- Span<T> sống trên stack. Không thể lưu nó vào field của class hoặc trả trực tiếp từ các method async.
- Đảm bảo an toàn kiểu: truy cập bộ nhớ mà không phải làm việc thủ công với con trỏ (trừ khi bạn dùng unsafe).
Tóm tắt về các khả năng mới của C# 14
C# 14 mở rộng làm việc với slices: range tiện lợi .. và index từ cuối ^1, pattern matching cải tiến cho mảng và slices và nhiều "syntactic sugar" khác. Sẽ quay lại phần này ở cuối.
2. Cách tạo Span<T>?
Bắt đầu với mảng đơn giản và tạo một span:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // từ phần tử thứ 3 (index 2) lấy 3 phần tử
Giờ midSpan trỏ tới các phần tử 30, 40, 50 — đây không phải bản sao, mà là view trên các phần tử "sống" của mảng. Thay đổi chúng qua span sẽ thay đổi mảng gốc.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Sẽ in 100
Tại sao không dùng slice mảng trực tiếp?
Slice truyền thống bằng LINQ tạo một mảng mới:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- ở đây tạo bản sao!
Điều này tốn thời gian và bộ nhớ. Span giải quyết vấn đề copy thừa — và không chỉ với số.
3. Nhiều cách tạo Span<T> hơn
Mảng tồn tại
Span<int> mySpan = numbers; // Chuyển đổi ngầm từ mảng sang Span
Một phần mảng
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 phần tử bắt đầu từ index 1: {20, 30, 40, 50}
Bộ nhớ stack (stackalloc)
Span<T> cho phép cấp phát mảng trên stack:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
Bộ nhớ stack nhanh và được giải phóng tự động khi thoát method. Nhưng kích thước phải hợp lý — vài megabyte trên stack là không chấp nhận được.
Chuỗi và ReadOnlySpan<char>
Chuỗi trong .NET là immutable, nên ta dùng ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Hãy ghép một ví dụ!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Toàn bộ lịch sử đơn hàng: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("In các đơn từ 2 đến 4 (indexes 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Mutate dữ liệu qua Span
recentOrders[1] = 999;
Console.WriteLine("Sau khi thay đổi qua Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Slice recentOrders thực sự làm việc trực tiếp với mảng — không phải bản sao.
5. Security, performance và checks
- Sử dụng bộ nhớ tiết kiệm: không copy thừa.
- Kiểm tra biên giới bảo vệ khỏi out-of-range.
- Tối ưu JIT mang lại truy cập rất nhanh (không cần unsafe).
Tương tác với method
Method có thể nhận Span<T> hoặc ReadOnlySpan<T>. Nếu không định sửa dữ liệu — dùng ReadOnlySpan<T>.
static int Sum(Span<int> slice)
{
int sum = 0;
foreach (var item in slice)
sum += item;
return sum;
}
int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12
6. Dùng range
Kể từ C# 8 có range .. và index từ cuối ^ — chúng hoạt động rất tốt với slices.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // indexes 2,3,4 - tức 30, 40, 50
Trong range, index bắt đầu được bao gồm, index kết thúc thì không.
Index từ cuối
int lastElement = arr[^1]; // phần tử cuối (60)
Span<int> lastTwo = arr[^2..]; // hai phần tử cuối (50, 60)
Range và chuỗi
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Lát này là ReadOnlySpan<char>; để có string gọi ToString().
7. Pattern hiện đại làm việc với slices (C# 14)
Các pattern mới làm đơn giản việc phân tích mảng và slices.
if (arr is [10, 20, .. var rest]) // .. bắt "đuôi" mảng
{
Console.WriteLine("Bắt đầu khớp, phần đuôi:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Mảng kết thúc bằng 50, 60");
So sánh: Array, ArraySegment và Span
| Loại | Mutable | Copy dữ liệu? | Có thể trên stack | Class/struct | Có thể làm field của class |
|---|---|---|---|---|---|
|
có | - | không | class | có |
|
có | không | không | struct | có |
|
có | không | có | struct | không |
|
không | không | có | struct | không |
Nói cách khác, Span<T> là sự tiến hóa của ArraySegment<T>: hiệu năng cao hơn và an toàn hơn.
8. Lỗi thường gặp và cạm bẫy
Cố gắng lưu Span<T> vào field của class. Không được: field sống trên heap, còn Span<T> phải sống trên stack. Dùng ArraySegment<T> hoặc chỉ số (indexes) để truy cập thay thế.
Trả/ lưu Span<T> từ các method async. Không được: async phá vỡ stack. Truyền dữ liệu khác — ví dụ mảng, Memory<T>/ReadOnlyMemory<T>.
Range sai khi tạo slice. Vượt ra ngoài biên sẽ ném exception lúc runtime. Luôn kiểm tra Length và biên trước khi tạo slice.
Quên rằng span phản chiếu dữ liệu gốc. Mọi thay đổi qua Span<T> thay đổi mảng gốc. Nếu cần bản sao độc lập — copy dữ liệu rõ ràng.
GO TO FULL VERSION