1. Introduzione
Tutto è iniziato dal desiderio degli sviluppatori .NET di velocizzare il lavoro con grandi quantità di dati e dare agli sviluppatori la possibilità di farlo nel modo più comodo e sicuro possibile. Prima è comparso Span<T>, che è una "finestra" su un'area di memoria contigua, capace di mostrare parte di un array, di una stringa o anche memoria allocata fuori da .NET (per esempio tramite P/Invoke).
Tuttavia Span<T> ha un vincolo potente: deve sempre vivere solo sullo stack. Non può essere conservato in campi di classi, non può essere restituito da metodi, non può essere passato tra metodi asincroni. La ragione è la sicurezza: se qualcuno tenesse una referenza a una memoria che non esiste più, l'applicazione andrebbe sicuramente in crash.
A volte è necessario restituire slice di dati dai metodi, conservarli in collezioni o campi delle classi, e anche usarli nelle API asincrone. Qui entra in scena Memory<T> — di fatto è la versione sicura "a lunga durata" di Span<T>, che può vivere sull'heap, essere passata tra thread, "vivere" in proprietà e oggetti e comportarsi come un normale oggetto .NET.
C'è anche un "fratello maggiore" — ReadOnlyMemory<T>, che, come puoi immaginare, non permette di modificare i dati sottostanti, ma consente di leggerli ovunque serva.
2. Qual è la differenza tra Span<T> e Memory<T>
Ecco una piccola tabella per confronto visivo:
|
|
|
|---|---|---|
| Dove vive | Solo sullo stack (stack only) | Sullo stack e sull'heap (heap/stack) |
| Può essere conservato in un campo | ❌ No | ✅ Sì |
| Può essere restituito da un metodo | ❌ No | ✅ Sì |
| Metodi asincroni/await | ❌ No | ✅ Sì |
| Modificabile | ✅ Esiste anche ReadOnlySpan<T> | ✅ Esiste anche ReadOnlyMemory<T> |
| Permette di fare slice dei dati | ✅ Sì | ✅ Sì |
Se devi "scorrere" rapidamente i dati dentro un metodo — usa Span<T>. Se devi restituire il risultato all'esterno o metterlo in un campo di una classe — usa Memory<T>. E se i dati sono solo in lettura — usa ReadOnlyMemory<T>.
3. Firma e struttura base di Memory<T>
Come sempre: Memory<T> è generic, cioè un tipo generico. Puoi creare Memory<int>, Memory<byte>, Memory<char> e persino Memory<MyType>. Dentro Memory<T> c'è una referenza a un array, a una stringa o a un'altra sorgente di dati, e anche l'indicazione del range (da quale indice e quanti elementi).
Per ottenere accesso veloce a Memory<T> per l'elaborazione, si usa la sua proprietà Span — così ottieni immediatamente un Span<T> che può essere usato all'interno di un metodo sincrono.
4. Come creare Memory<T>: pratica
Esempio 1. Creazione da un array
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Memory<int> memory = new Memory<int>(numbers); // L'intero array
// Puoi prendere uno "slice" — parte dell'array
Memory<int> slice = memory.Slice(2, 3); // elementi 2, 3 e 4
Esempio 2. Creazione da una stringa (tramite Memory<char>)
string text = "Privet, mir!";
Memory<char> charMemory = text.AsMemory(); // L'intero testo come memoria
Memory<char> subMemory = charMemory.Slice(7, 3); // dal carattere 7, 3 caratteri ("mir")
Esempio 3. Uso di ReadOnlyMemory<T>
Esattamente lo stesso, solo protetto dalle modifiche:
int[] data = { 10, 20, 30, 40 };
ReadOnlyMemory<int> readOnly = data; // Non permetterà modifiche tramite questo oggetto
5. Conversione tra Memory<T> e Span<T>
Non si può lavorare con Memory<T> con la stessa flessibilità e velocità di Span<T> direttamente — è pensato per scopi diversi. Ma quando hai davvero bisogno di processare velocemente un pezzo di memoria, puoi ottenere uno Span<T> "istantaneo" dalla memoria tramite la proprietà Span:
void ProcessData(Memory<int> memory)
{
Span<int> span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
span[i] += 100;
}
}
Nota: Span<T> funziona solo all'interno del metodo. Se provi a restituirlo all'esterno, il compilatore segnalerà un errore.
Con ReadOnlyMemory<T> è analogo, solo che ottieni un ReadOnlySpan<T> che non ti permette di modificare i dati:
void PrintData(ReadOnlyMemory<int> memory)
{
ReadOnlySpan<int> roSpan = memory.Span;
foreach (var item in roSpan)
Console.WriteLine(item);
}
6. Uso in scenari reali
Elaborazione asincrona dei dati
Qui Memory<T> mostra il suo vero valore. Può essere usato nei metodi asincroni! Per esempio, lettura asincrona da file:
using System.IO;
using System.Threading.Tasks;
public async Task ReadFileAsync(string path)
{
byte[] buffer = new byte[4096];
using var stream = File.OpenRead(path);
int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
// Ora puoi lavorare con buffer
}
Qui AsMemory passa il buffer direttamente al metodo asincrono, senza problemi di scope o di distruzione della memoria, come sarebbe il caso con Span<T>.
Conservare slice di dati in proprietà e campi
A volte serve una classe che conserva un "pezzo" di un grande array per uso successivo:
class DataChunk
{
public Memory<byte> Data { get; }
public DataChunk(Memory<byte> data)
{
Data = data;
}
}
7. Uso con collezioni, stringhe e array
Con array
Molto spesso:
byte[] bytes = { 1, 2, 3, 4, 5 };
Memory<byte> mem = bytes; // L'intero array
Memory<byte> part = mem.Slice(2); // Dal terzo elemento fino alla fine
Con stringhe
Tramite AsMemory():
string hello = "Hello, Memory!";
ReadOnlyMemory<char> mem = hello.AsMemory(6, 6); // "Memory"
Con collezioni (per esempio List<T>)
Non puoi creare direttamente Memory<T> da una List<T>. Solo passando per un array:
List<int> list = new List<int> { 1, 2, 3 };
Memory<int> mem = list.ToArray(); // Copia, non riferimento!
Attenzione: se vuoi evitare copie — tieni i dati in un array.
8. Errori tipici quando si lavora con Memory<T>
Errore №1: tentare di usare Span<T> invece di Memory<T> nei campi di una classe. Non si può conservare Span<T> nei campi di una classe, perché è legato allo stack. Il compilatore segnalerà un errore. Usa Memory<T> per conservare nell'heap.
Errore №2: aspettarsi una copia dei dati quando fai slice. Memory<T> non copia i dati, crea una "finestra" sull'array esistente. Se modifichi i dati tramite un Memory<T>, questo si riflette su tutti gli altri riferimenti alla stessa memoria.
Errore №3: tentare di creare Memory<T> direttamente da List<T>. Memory<T> funziona solo con array, perché List<T> può cambiare la posizione dei suoi dati in memoria. Trasforma la list in array con ToArray().
Errore №4: ignorare ReadOnlyMemory<T> per dati immutabili. Se i dati non devono essere modificati, usa ReadOnlyMemory<T> invece di Memory<T> per maggiore sicurezza.
GO TO FULL VERSION