1. Introduction
Imaginez que vous avez un gros tableau de données, par exemple des bytes lus depuis un fichier. Vous voulez traiter un fragment — lire une partie comme une chaîne ou simplement passer un segment de données à une méthode. Dans le style "classique" vous copieriez ces bytes dans un nouveau tableau — c'est lent et ça consomme de la mémoire. Et si ces fragments sont des dizaines ou des centaines ?
C'est là qu'intervient le héros — Span<T>. C'est une structure spéciale qui décrit une vue (view) sur un tableau (ou même sur n'importe quel bloc mémoire contigu) sans copier les données, en pointant simplement sur la zone désirée. Important : Span<T> n'est pas une nouvelle collection, mais une "fenêtre" sécurisée sur un tableau existant !
Caractéristiques principales et limitations de Span<T>
- Span<T> est une structure (value type) représentant une "fenêtre" sur la mémoire et ne copie pas les éléments.
- Ne référence que des zones mémoire contiguës : tableau, partie de tableau, bloc alloué avec stackalloc, mémoire d'une chaîne via des méthodes spéciales, ou mémoire en code unsafe.
- Span<T> vit sur la stack. On ne peut pas le stocker dans un champ de classe ni le retourner directement depuis des méthodes async.
- Garantit la sécurité des types : accès mémoire sans manipulation manuelle de pointeurs (si vous n'utilisez pas unsafe).
Bref sur les nouvelles fonctionnalités de C# 14
C# 14 étend le travail avec les slices : les ranges pratiques .. et les indices depuis la fin ^1, des patterns de matching améliorés pour les tableaux et les slices et d'autres "sucres syntaxiques". On y reviendra à la fin.
2. Comment créer un Span<T> ?
Commençons par un tableau simple et créons un span :
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // prendre 3 éléments à partir du 3e élément (index 2)
Maintenant midSpan pointe sur les éléments 30, 40, 50 — ce n'est pas une copie mais une vue sur les éléments "vivants" du tableau. En les modifiant via le span, on modifie le tableau original.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Affichera 100
Pourquoi ne pas utiliser des slices de tableau classiques ?
Le slice classique avec LINQ crée un nouveau tableau :
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- ici une copie est créée !
C'est coûteux en temps et en mémoire. Span résout le problème des copies inutiles — et fonctionne pas seulement avec des nombres.
3. Encore plus de façons de créer un Span<T>
Tableau existant
Span<int> mySpan = numbers; // Conversion implicite d'un tableau en Span
Partie d'un tableau
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 éléments à partir de l'index 1 : {20, 30, 40, 50}
Mémoire sur la stack (stackalloc)
Span<T> permet d'allouer des tableaux sur la stack :
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
La mémoire sur la stack est rapide et libérée automatiquement à la sortie de la méthode. Mais le volume doit rester raisonnable — des mégaoctets sur la stack sont inacceptables.
Chaînes et ReadOnlySpan<char>
Les chaînes en .NET sont immuables, donc on utilise ReadOnlySpan<char> :
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Construisons un exemple !
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Tout l'historique des commandes : ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Afficher les commandes de la 2e à la 4e (indices 1-3) :");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Muter les données via Span
recentOrders[1] = 999;
Console.WriteLine("Après modification via Span :");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Le slice recentOrders fonctionne réellement directement sur le tableau — ce n'est pas une copie.
5. Sécurité, performance et vérifications
- Usage économe de la mémoire : pas de copies inutiles.
- Les vérifications de bornes protègent contre les accès hors limite du tableau.
- Les optimisations JIT donnent un accès très rapide (sans unsafe).
Interaction avec les méthodes
Les méthodes peuvent accepter Span<T> ou ReadOnlySpan<T>. Si vous ne prévoyez pas de modifier les données — utilisez 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. Utilisation des ranges (range)
Depuis C# 8, les ranges .. et les indices depuis la fin ^ sont disponibles — ils fonctionnent très bien avec les slices.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // indices 2,3,4 - donc 30, 40, 50
Dans un range l'index de début est inclus, l'index de fin est exclu.
Indices depuis la fin
int lastElement = arr[^1]; // dernier élément (60)
Span<int> lastTwo = arr[^2..]; // les deux derniers éléments (50, 60)
Ranges et chaînes
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Ce slice est un ReadOnlySpan<char> ; pour obtenir une chaîne, appelez ToString().
7. Patterns modernes pour travailler avec des slices (C# 14)
Les nouveaux patterns simplifient le déconstruction des tableaux et des slices.
if (arr is [10, 20, .. var rest]) // .. capture la "queue" du tableau
{
Console.WriteLine("Le début correspond, la queue :");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Le tableau se termine par 50, 60");
Comparaison : Array, ArraySegment et Span
| Type | Mutable | Copie les données ? | Peut être sur la stack | Classe/structure | Peut être champ de classe |
|---|---|---|---|---|---|
|
oui | - | non | classe | oui |
|
oui | non | non | structure | oui |
|
oui | non | oui | structure | non |
|
non | non | oui | structure | non |
En d'autres termes, Span<T> est l'évolution de ArraySegment<T> : plus performant et plus sûr.
8. Erreurs typiques et pièges
Tentative de stocker un Span<T> dans un champ de classe. Impossible : les champs vivent dans le heap, et Span<T> doit vivre sur la stack. Utilisez ArraySegment<T> ou des indices pour adresser les données.
Retourner/stocke r un Span<T> depuis des méthodes async. Impossible : l'async casse la stack. Transmettez les données autrement — par exemple comme un tableau, Memory<T>/ReadOnlyMemory<T>.
Mauvais range lors de la création d'un slice. Dépasser les bornes provoque une exception à l'exécution. Vérifiez toujours la longueur et les bornes avant de créer un slice.
Oublier que le span reflète les données sources. Toute modification via Span<T> change le tableau original. Si vous avez besoin d'une copie indépendante — copiez explicitement les données.
GO TO FULL VERSION