CodeGym /Kursy /C# SELF /Nowe możliwości Span<T>

Nowe możliwości Span<T>

C# SELF
Poziom 65, Lekcja 3
Dostępny

1. Wprowadzenie

Wyobraź sobie, że masz dużą tablicę danych, na przykład bajty odczytane z pliku. Chcesz przetworzyć jej fragment — odczytać część jako string albo po prostu przekazać kawałek danych do metody. W "starym" stylu kopiowałbyś te bajty do nowej tablicy — to wolne i pamięciożerne. A jeśli takich fragmentów są dziesiątki albo setki?

I tu pojawia się bohater — Span<T>. To specjalna struktura, która opisuje wycinek (view) nad tablicą (albo nawet nad dowolnym ciągłym obszarem pamięci), bez kopiowania danych, po prostu wskazując odpowiedni zakres. Ważne: Span<T> to nie nowa kolekcja, a bezpieczne "okno" w istniejącą tablicę!

Główne cechy i ograniczenia Span<T>

  • Span<T> to struktura (value type), reprezentująca "okno" w pamięć i nie kopiująca elementów.
  • Odwołuje się tylko do ciągłego obszaru pamięci: tablica, część tablicy, blok z stackalloc, pamięć stringa przez specjalne metody, albo pamięć w unsafe-kodzie.
  • Span<T> żyje na stosie. Nie wolno przechowywać go w polu klasy ani zwracać bezpośrednio z async-metod.
  • Gwarantuje bezpieczeństwo typów: dostęp do pamięci — bez ręcznej pracy z wskaźnikami (o ile nie używasz unsafe).

Krótko o nowych możliwościach C# 14

C# 14 rozszerza pracę z wycinkami: wygodne zakresy .. i indeksy od końca ^1, ulepszone patterny dopasowania dla tablic i wycinków oraz inne "cukierki" składniowe. Do tego wrócimy na końcu.

2. Jak stworzyć Span<T>?

Zacznijmy od prostej tablicy i stwórzmy span:

int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // od 3-ciego elementu (indeks 2) wziąć 3 elementy

Teraz midSpan wskazuje elementy 30, 40, 50 — to nie kopia, tylko widok na "żywe" elementy tablicy. Zmieniając je przez span, zmieniasz oryginalną tablicę.

midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Wypisze 100

Dlaczego nie używać wycinków tablic bezpośrednio?

Klasyczny wycinek LINQ tworzy nową tablicę:

int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- tutaj powstaje kopia!

To kosztuje czas i pamięć. Span rozwiązuje problem zbędnych kopii — i działa nie tylko z liczbami.

3. Jeszcze więcej sposobów tworzenia Span<T>

Istniejąca tablica

Span<int> mySpan = numbers; // Niejawna konwersja z tablicy na Span

Część tablicy

int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 elementy zaczynając od indeksu 1: {20, 30, 40, 50}

Pamięć stosu (stackalloc)

Span<T> pozwala alokować tablice na stosie:

Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = (byte)i;

Pamięć stosu jest szybka i zwalniana automatycznie po wyjściu z metody. Ale rozmiar powinien być rozsądny — megabajty na stosie są niedopuszczalne.

Stringi i ReadOnlySpan<char>

Stringi w .NET są niemutowalne, więc używamy ReadOnlySpan<char>:

string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8);   // "C# world"
Console.WriteLine(span.ToString());                // C# world

4. Złóżmy przykład!

using System;

class Program
{
    static void Main()
    {
        int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
        Console.WriteLine("Cała historia zamówień: ");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
        
        Console.WriteLine("Wypisać zamówienia od 2 do 4 (indeksy 1-3):");
        Span<int> recentOrders = orderTotals.AsSpan(1, 3);
        foreach (int t in recentOrders)
            Console.Write(t + " ");
        Console.WriteLine();
        
        // Mutujemy dane przez Span
        recentOrders[1] = 999;
        Console.WriteLine("Po zmianie przez Span:");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
    }
}

Wycinek recentOrders działa bezpośrednio z tablicą — to nie kopia.

5. Bezpieczeństwo, wydajność i sprawdzenia

  • Oszczędne gospodarowanie pamięcią: brak zbędnych kopii.
  • Sprawdzenia zakresów chronią przed wyjściem poza tablicę.
  • Optymalizacje JIT dają bardzo szybki dostęp (bez unsafe).

Interakcja z metodami

Metody mogą przyjmować Span<T> lub ReadOnlySpan<T>. Jeśli nie planujesz modyfikować danych — użyj 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. Użycie zakresów (range)

Począwszy od C# 8 dostępne są zakresy .. i indeksy od końca ^ — świetnie działają ze wycinkami.

int[] arr = { 10, 20, 30, 40, 50, 60 };

Span<int> span = arr[2..5]; // indeksy 2,3,4 - czyli 30, 40, 50

W zakresie indeks początkowy jest włączony, końcowy — wyłączony.

Indeksy od końca

int lastElement = arr[^1];     // ostatni element (60)
Span<int> lastTwo = arr[^2..]; // dwa ostatnie elementy (50, 60)

Zakresy i stringi

string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"

Ten wycinek to ReadOnlySpan<char>; żeby dostać string, wywołaj ToString().

7. Nowoczesne patterny pracy z wycinkami (C# 14)

Nowe patterny upraszczają rozbiórkę tablic i wycinków.

if (arr is [10, 20, .. var rest]) // .. przechwytuje "ogon" tablicy
{
    Console.WriteLine("Początek pasuje, ogon:");
    foreach (var x in rest)
        Console.WriteLine(x);
}
if (arr is [.., 50, 60])
    Console.WriteLine("Tablica kończy się na 50, 60");

Porównanie: Tablica, ArraySegment i Span

Typ Mutowalny Kopiuje dane? Można na stosie Klasa/struktura Można robić pola klasy
T[]
tak - nie klasa tak
ArraySegment<T>
tak nie nie struktura tak
Span<T>
tak nie tak struktura nie
ReadOnlySpan<T>
nie nie tak struktura nie

Innymi słowy, Span<T> to ewolucja ArraySegment<T>: bardziej wydajny i bezpieczny.

8. Typowe błędy i pułapki

Próba przechowywania Span<T> w polu klasy. Nie wolno: pola żyją na heapie, a Span<T> musi żyć na stosie. Użyj ArraySegment<T> albo indeksów do adresowania.

Zwracanie/przechowywanie Span<T> z async-metod. Nie wolno: asynchroniczność przerywa stos. Przekaż dane inaczej — np. jako tablicę, Memory<T>/ReadOnlyMemory<T>.

Nieprawidłowy zakres przy tworzeniu wycinka. Wyjście poza granice powoduje wyjątek w czasie wykonywania. Zawsze sprawdzaj długość i granice przed formowaniem wycinka.

Zapomnienie, że span odzwierciedla oryginalne dane. Każda zmiana przez Span<T> zmienia oryginalną tablicę. Jeśli potrzebujesz niezależnej kopii — skopiuj dane jawnie.

2
Zadanie
C# SELF, poziom 65, lekcja 3
Niedostępne
Użycie zakresów i indeksowania od końca do wyświetlania części tablicy
Użycie zakresów i indeksowania od końca do wyświetlania części tablicy
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION