1. Introduzione
Quando lavori con array, stringhe e buffer di byte, spesso serve “guardare” una parte di quei dati. Per esempio, estrarre una sottostringa, prendere uno slice di un array, elaborare una porzione di un flusso in ingresso. Nelle vecchie versioni di .NET bisognava o copiare i dati (creare un nuovo array/sottostringa), o scrivere codice che scorre l'array dall'indice di inizio a quello di fine. Non è il massimo né in termini di performance né di leggibilità.
Ecco un esempio del vecchio approccio: dobbiamo passare a un metodo solo una parte di un grande array:
// Approccio vecchio — copiamo parte dell'array (inefficiente!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // viene creato un nuovo array
Quindi, se vogliamo passare efficacemente uno “slice” di un array (o anche di una stringa) senza creare oggetti extra, gli strumenti C# tradizionali perdono nettamente, specialmente con grandi volumi di dati.
Ed ecco il protagonista della giornata — Span<T>!
2. Che cos'è Span<T>? Idea principale
Span<T> è un tipo che rappresenta un'area contigua di memoria di elementi dello stesso tipo T. Lo scopo è darti un modo veloce, sicuro ed efficiente per lavorare con porzioni di array, stringhe, struct e anche con memoria non gestita (per esempio memoria allocata fuori dal managed runtime .NET).
La caratteristica principale di Span<T> è lo “slice senza creare nuovi array”. Immagina un righello che ti permette di misurare qualsiasi porzione dello stesso array senza copiare i dati e con un rischio minimo di sbagliare gli indici.
In breve:
- Span<T> — una “finestra” o “vista” su un pezzo di memoria che puoi manipolare in modo comodo e sicuro.
- Nessuna allocazione di nuova memoria — risparmi risorse e riduci il lavoro della GC.
- Funziona non solo con array, ma anche con segmenti di stringhe, blocchi allocati con stackalloc e persino con memoria non gestita.
- Non si può conservare nei campi di una classe normale: è un tipo di sola stack (stack-only struct).
Perché è importante?
Nelle attività ad alte prestazioni (parsing di file, elaborazione di grandi buffer, crittografia, serializzazione) risparmiare anche poche copie di array può dare un grande boost di velocità e ridurre la pressione sul garbage collector (GC). Inoltre dimostrerai ai colleghi che conosci il C# e .NET moderni!
3. Uso base di Span<T>: il primo slice
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Creare uno Span su una parte dell'array (per esempio elementi dall'indice 2 al 5 incluso)
Span<int> middle = new Span<int>(numbers, 2, 4); // indici: 2, 3, 4, 5
// Vediamo il sotto-array: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// Modificare lo Span modifica l'array originale!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
Attenzione! Span<T> non copia i dati, punta solo a un “pezzo” dell'array. Tutte le modifiche sono visibili sia nell'array originale sia nello Span.
4. Modi principali per creare Span<T>
Da un array:
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // lunghezza completa
Span<int> slice = arr.AsSpan(1, 3); // elementi 20, 30, 40
Da una parte dell'array:
Span<int> part = new Span<int>(arr, 2, 2); // elementi 30, 40
stackalloc: allocazione sulla stack (estremamente veloce e non finisce nell'heap):
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
Con il metodo .Slice():
Span<int> subSpan = span.Slice(1, 2); // elementi 20, 30
Schema visivo dello “slice”
Array originale: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Limitazioni e caratteristiche di Span<T>
- Solo stack! Non puoi conservarlo come campo di una classe “normale” o usarlo come parte di una closure — è un tipo di stack.
- Non si può usare come campo di una classe né restituirlo da metodi async (il compilatore genererà un errore).
- Non si può catturare in lambda/metodi anonimi — usalo “qui e ora”.
- Non si può serializzare direttamente o passarlo tra thread.
Questo perché Span può puntare a qualsiasi area di memoria, e se finisse per “trasferirsi” nell'heap si potrebbero creare stati non sicuri.
6. Immutabilità: ReadOnlySpan<T>
A volte vuoi solo “guardare” una porzione di memoria senza modificarla. Per questo c'è la versione immutabile — ReadOnlySpan<T>.
string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n
// letters[0] = 'Z'; // Errore: l'indicizzatore è solo in lettura!
Scenario classico — passare in modo sicuro un “pezzo” di stringa o array a qualcuno senza permettere modifiche.
7. Esempio pratico: “Tagliamo” array e stringhe
Immaginiamo un analizzatore che da una stringa grande estrae una sottostringa, cerca numeri e ne ritorna la somma (senza copie inutili al primo slice):
using System;
class Program
{
static void Main()
{
// Supponiamo che l'utente abbia inserito una lunga stringa di numeri separati da spazi
string input = "12 34 56 78 90 123 456 789";
// Dobbiamo calcolare la somma dei numeri solo dal "centro", per esempio 56 78 90
// Prendiamo la sottostringa (ma non la copiamo!)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // gli indici possono essere calcolati dinamicamente
// Parse dei numeri tramite Split (crea un array temporaneo)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"Somma dei numeri centrali: {sum}");
}
}
Le librerie moderne di parsing CSV e JSON usano Span per alte prestazioni con grandi volumi di dati testuali — ora sai su cosa si basa il loro “metodo magico”.
8. Sfumature utili
Span vs copia di array
// Vecchio metodo: copiamo un pezzo di array
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // abbiamo creato un nuovo array da 1000 elementi
// Nuovo metodo: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // non copiamo nulla!
bestThousand[0] = 42; // cambia anche in arr
La differenza diventa evidente con parsing intensivo di file, gestione di buffer di rete e lavoro con dati binari.
Uso in scenari reali: perché conoscere Span
- Parsing ad alte prestazioni e elaborazione di dati testuali/binari. Le librerie moderne di serializzazione (per esempio, System.Text.Json, Span nella documentazione Microsoft) usano Span per accelerare l'esecuzione.
- Buffering e lettura di file (tagliare grandi buffer senza copiare).
- Elaborazione in contesti con memoria limitata (embedded, IoT) — documentazione ufficiale su Memory/Spans.
- Algoritmi su immagini e audio, dove la velocità e l'assenza di allocazioni extra sono critiche.
- Velocizzare il parsing di CSV, JSON, XML con Span — specialmente in .NET 8/9.
Alle interview le domande su Span sono apparse da quando è arrivato in .NET Core 2.1+, e in .NET 9 è sempre più richiesto come conoscenza desiderabile.
Schema visivo: dove c'è Span e dove l'array
+--------------------+
| int[] array |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> "finestra di memoria" (slice)
Span<T> non è un array separato, ma una “lente trasparente” su una parte dei dati.
Differenze rispetto ad altre collezioni: confronto
| Tipo | Contiene dati? | Si possono modificare gli elementi? | Si può cambiare la dimensione? | Copia quando si fa lo slice? | Dove vive? |
|---|---|---|---|---|---|
|
Sì | Sì | No | Sì (tramite .Take) | Heap |
|
Sì | Sì | Sì | Sì | Heap |
|
No | Sì | No | No | Stack |
|
No | No | No | No | Stack |
9. Errori tipici lavorando con Span/ReadOnlySpan
Errore №1: provare a conservare uno Span come campo di una classe. Il compilatore dirà “Span type may not be used in this context”. È voluto: è pericoloso memorizzare uno Span in un campo.
Errore №2: ritornare uno Span da un metodo async. Non si può fare, perché gli async-metodi possono finire nell'heap. Usa invece un array o un altro tipo.
Errore №3: dimenticare che le modifiche fatte tramite lo Span si riflettono sull'array originale. Questo può modificare dati “fuori” e causare comportamenti inaspettati.
GO TO FULL VERSION