1. Introduzione
Immagina di avere un grande array di dati, per esempio bytes letti da un file. Vuoi elaborare un frammento — leggere una parte come stringa o semplicemente passare un segmento di dati a un metodo. Nello "stile vecchio" avresti copiato quei bytes in un nuovo array — è lento e consuma memoria. E se quei frammenti sono decine o centinaia?
Ed ecco il protagonista — Span<T>. È una struttura speciale che descrive una vista (view) su un array (o su qualsiasi blocco contiguo di memoria), senza copiare i dati, indicando semplicemente l'area desiderata. Importante: Span<T> non è una nuova collection, ma una "finestra" sicura su un array esistente!
Caratteristiche principali e limiti di Span<T>
- Span<T> è una struct (value type) che rappresenta una "finestra" sulla memoria e non copia gli elementi.
- Si riferisce solo a un blocco contiguo di memoria: array, parte di array, blocco allocato con stackalloc, memoria della stringa tramite metodi speciali, oppure memoria in codice unsafe.
- Span<T> vive sullo stack. Non lo puoi conservare in un campo di una classe né restituirlo direttamente da metodi async.
- Garantisce la sicurezza dei tipi: accesso alla memoria senza lavorare manualmente con i puntatori (se non usi unsafe).
Brevemente sulle nuove feature di C# 14
C# 14 estende il lavoro con i slice: comodi range .. e indici dalla fine ^1, pattern matching migliorati per array e slice e altro "sugar" sintattico. Torneremo su questo alla fine.
2. Come creare Span<T>?
Iniziamo da un array semplice e creiamo uno span:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // prendere 3 elementi a partire dal 3° (indice 2)
Ora midSpan punta agli elementi 30, 40, 50 — non è una copia, ma una vista sugli elementi "vivi" dell'array. Modificandoli tramite lo span, modifichi l'array originale.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Stamperà 100
Perché non usare slice di array che copiano?
Il classico slice con LINQ crea un nuovo array:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- qui si crea una copia!
Questo è costoso in tempo e memoria. Span risolve il problema delle copie inutili — e funziona non solo con numeri.
3. Altri modi per creare Span<T>
Array esistente
Span<int> mySpan = numbers; // Conversione implicita da array a Span
Parte di un array
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 elementi a partire dall'indice 1: {20, 30, 40, 50}
Memoria dello stack (stackalloc)
Span<T> permette di allocare array sullo stack:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
La memoria dello stack è veloce e viene liberata automaticamente all'uscita dal metodo. Ma la dimensione deve essere ragionevole — megabyte nello stack non sono accettabili.
Stringhe e ReadOnlySpan<char>
Le stringhe in .NET sono immutabili, quindi usiamo ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Mettiamo insieme un esempio!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Tutta la cronologia degli ordini: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Stampare gli ordini dal 2° al 4° (indici 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Mutiamo i dati tramite Span
recentOrders[1] = 999;
Console.WriteLine("Dopo la modifica tramite Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Il slice recentOrders lavora realmente sull'array — non è una copia.
5. Sicurezza, performance e controlli
- Uso parsimonioso della memoria: niente copie inutili.
- Controlli dei limiti proteggono dall'out-of-bounds.
- Le ottimizzazioni JIT forniscono accesso molto veloce (senza unsafe).
Interazione con i metodi
I metodi possono accettare Span<T> o ReadOnlySpan<T>. Se non prevedi di modificare i dati — usa 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. Uso dei range
A partire da C# 8 sono disponibili i range .. e gli indici dalla fine ^ — funzionano molto bene con i slice.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // indici 2,3,4 - cioè 30, 40, 50
Nel range l'indice iniziale è incluso, quello finale è escluso.
Indici dalla fine
int lastElement = arr[^1]; // ultimo elemento (60)
Span<int> lastTwo = arr[^2..]; // gli ultimi due elementi (50, 60)
Range e stringhe
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Questo slice è un ReadOnlySpan<char>; per ottenere una stringa, chiama ToString().
7. Pattern moderni per lavorare con slice (C# 14)
I nuovi pattern rendono più semplice analizzare array e slice.
if (arr is [10, 20, .. var rest]) // .. cattura il "resto" dell'array
{
Console.WriteLine("L'inizio coincide, il resto:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("L'array termina con 50, 60");
Confronto: Array, ArraySegment e Span
| Tipo | Mutabile | Copia i dati? | Si può allocare sullo stack | Class/struct | Si può tenere come campo di classe |
|---|---|---|---|---|---|
|
sì | - | no | class | sì |
|
sì | no | no | struct | sì |
|
sì | no | sì | struct | no |
|
no | no | sì | struct | no |
In altre parole, Span<T> è l'evoluzione di ArraySegment<T>: più performante e sicuro.
8. Errori tipici e insidie
Tentare di conservare Span<T> in un campo di una classe. Non è permesso: i campi vivono sull'heap, mentre Span<T> deve vivere sullo stack. Usa ArraySegment<T> o indici per indirizzare i dati.
Restituire/tenere Span<T> da metodi async. Non è consentito: l'async rompe lo stack. Passa i dati in altro modo — ad esempio come array, Memory<T>/ReadOnlyMemory<T>.
Range non valido quando si crea il slice. L'uscita dai limiti genera un'eccezione a runtime. Controlla sempre length e confini prima di creare il slice.
Dimenticare che lo span riflette i dati originali. Qualsiasi modifica tramite Span<T> cambia l'array originale. Se ti serve una copia indipendente — copia i dati esplicitamente.
GO TO FULL VERSION