CodeGym /Các khóa học /C# SELF /Những khả năng mới của Spa...

Những khả năng mới của Span<T>

C# SELF
Mức độ, Bài học
Có sẵn

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
T[]
- không class
ArraySegment<T>
không không struct
Span<T>
không struct không
ReadOnlySpan<T>
không không 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.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng range và chỉ số từ cuối để hiển thị một phần của mảng
Sử dụng range và chỉ số từ cuối để hiển thị một phần của mảng
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION