1. Introduction
Quand vous travaillez avec des tableaux, des chaînes et des buffers d'octets, il arrive souvent qu'il faille "regarder" une partie de ces données. Par exemple, extraire une sous-chaîne, prendre un slice d'un tableau, traiter une portion d'un flux entrant. Dans les anciennes versions de .NET il fallait soit copier les données (créer un nouveau tableau/sous-chaîne), soit écrire du code qui parcourt le tableau depuis un index jusqu'à un autre. Tout ça n'est pas génial ni pour la performance ni pour la lisibilité.
Voici un exemple d'approche ancienne : il faut passer à une méthode seulement une partie d'un grand tableau :
// Ancienne approche — on copie une partie du tableau (inefficace !)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // un nouveau tableau est créé
Donc, si on veut passer efficacement un "slice" de tableau (ou même une portion de chaîne) sans créer d'objets inutiles, les anciens moyens de C# perdent clairement, surtout avec de gros volumes de données.
Et c'est là que le héros du jour arrive — Span<T> !
2. Qu'est-ce que Span<T> ? Idée principale
Span<T> est un type qui représente une zone contiguë de mémoire d'un même type T. Son but est de vous donner un moyen rapide, sûr et efficace de travailler sur des portions de tableaux, de chaînes, de structures, ainsi que sur de la mémoire non gérée (par ex. mémoire allouée en dehors du runtime .NET).
La grosse caractéristique de Span<T> est le "slice sans création de nouveaux tableaux". Imaginez une règle avec laquelle vous pouvez mesurer n'importe quel segment d'un tableau sans copier les données et avec un minimum de risques d'erreur sur les indices.
En bref :
- Span<T> — une "fenêtre" ou une "vue" sur un morceau de mémoire, manipulable de façon pratique et sûre.
- Pas d'allocation d'une nouvelle mémoire — économie de ressources et moins de travail pour le GC.
- Fonctionne non seulement avec les tableaux, mais aussi avec des segments de chaînes, des blocs alloués via stackalloc et même avec de la mémoire non managée.
- Ne peut pas être stocké dans des champs d'une classe ordinaire : c'est un type stack-only (struct stack-only).
Pourquoi c'est important ?
Dans les tâches à haute performance (parsing de fichiers, traitement de gros buffers, cryptographie, sérialisation) économiser ne serait-ce que quelques copies de tableaux peut apporter un énorme gain de vitesse et réduire la charge sur le ramasse-miettes (GC). Et puis vous montrerez à vos collègues que vous maîtrisez le C# moderne et .NET !
3. Utilisation basique de Span<T> : premier slice
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Créer un Span sur une partie du tableau (par exemple éléments de 2 à 5 inclus)
Span<int> middle = new Span<int>(numbers, 2, 4); // indices : 2, 3, 4, 5
// On voit le sous-tableau : 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// Modifier le Span modifie le tableau d'origine !
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
Important ! Span<T> ne copie pas les données, il pointe juste vers un "morceau" du tableau. Toutes les modifications sont visibles à la fois dans le tableau d'origine et dans le Span.
4. Principales manières de créer un Span<T>
À partir d'un tableau :
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // longueur complète
Span<int> slice = arr.AsSpan(1, 3); // éléments 20, 30, 40
À partir d'une partie du tableau :
Span<int> part = new Span<int>(arr, 2, 2); // éléments 30, 40
stackalloc : allocation sur la pile (extrêmement rapide et n'atterrit pas dans le "heap") :
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
Via la méthode .Slice() :
Span<int> subSpan = span.Slice(1, 2); // éléments 20, 30
Schéma visuel du "slice"
Tableau initial : 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Limitations et particularités de Span<T>
- Stack-only ! Ne peut pas être stocké dans des champs "ordinaires" de classe ni faire partie d'une closure — c'est un type stack-only.
- Ne peut pas être utilisé comme champ d'une classe ou retourné depuis des méthodes async (le compilateur lèvera une erreur).
- Ne peut pas être capturé dans des lambdas/méthodes anonymes — utilisez-le "ici et maintenant".
- Ne peut pas être sérialisé directement ou passé entre des threads.
Ceci est lié au fait que Span peut pointer sur n'importe quelle zone de mémoire, et si jamais il "déménage" sur le heap, on peut obtenir des états dangereux.
6. Immutabilité : ReadOnlySpan<T>
Parfois il faut "regarder" une portion de mémoire sans la modifier. Pour ça il y a la variante immuable — 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'; // Erreur : l'indexeur est en lecture seule !
Scénario classique — transmettre de manière sûre un "morceau" de chaîne ou de tableau quelque part où on ne doit pas (et ne veut pas) le modifier.
7. Pratique : exemple — on "slice" des tableaux et des chaînes
Supposons un analyseur de données qui extrait d'une grande chaîne une sous-chaîne, recherche des nombres dedans et renvoie leur somme (sans copies inutiles lors du slice initial) :
using System;
class Program
{
static void Main()
{
// Supposons que l'utilisateur a entré une longue chaîne de nombres séparés par des espaces
string input = "12 34 56 78 90 123 456 789";
// On doit calculer la somme des nombres seulement du "centre", par exemple : 56 78 90
// On extrait la sous-chaîne (sans la copier !)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // les indices peuvent être calculés dynamiquement
// On parse les nombres via Split (ça crée un tableau temporaire)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"Somme des nombres centraux : {sum}");
}
}
Les bibliothèques modernes de parsing CSV et JSON utilisent Span pour des performances élevées sur de gros volumes de données textuelles — maintenant vous savez sur quoi repose leur "magie".
8. Nuances utiles
Span vs copie de tableaux
// Ancienne façon : on copie un morceau du tableau
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // on a créé un nouveau tableau de 1000 éléments
// Nouvelle façon : Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // pas de copie du tout !
bestThousand[0] = 42; // change aussi dans arr
La différence est particulièrement visible lors de parsings intensifs de fichiers, du traitement de buffers réseau ou du travail avec des données binaires.
Usage en situations réelles : pourquoi connaître Span
- Parsing et traitement hautement performants de données textuelles/binary. Les bibliothèques modernes de sérialisation (par ex. System.Text.Json, Span dans la doc Microsoft) utilisent Span pour accélérer le traitement.
- Buffering et lecture de fichiers (découper de gros buffers sans copie).
- Traitement de données avec contraintes mémoire (embedded, IoT) — documentation officielle sur Memory/Spans.
- Algorithmes pour images et audio où la vitesse et l'absence d'allocations sont critiques.
- Accélérer le parsing CSV, JSON, XML grâce à Span — surtout en .NET 8/9.
En entretien on commence à poser des questions sur Span depuis sa sortie dans .NET Core 2.1+, et dans .NET 9 c'est de plus en plus attendu comme connaissance.
Schéma visuel : où est Span et où est le tableau
+--------------------+
| int[] tableau |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> "fenêtre mémoire" (slice)
Span<T> n'est pas un tableau séparé, mais une "lentille transparente" sur une partie des données.
Différence avec d'autres collections : comparaison
| Type | Contient les données ? | Peut modifier les éléments ? | Peut changer de taille ? | Copie lors du slice ? | Où vit ? |
|---|---|---|---|---|---|
|
Oui | Oui | Non | Oui (via .Take) | Heap |
|
Oui | Oui | Oui | Oui | Heap |
|
Non | Oui | Non | Non | Stack |
|
Non | Non | Non | Non | Stack |
9. Erreurs typiques avec Span/ReadOnlySpan
Erreur n°1 : essayer de stocker un Span comme champ d'une classe. Le compilateur renverra l'erreur "Span type may not be used in this context". C'est volontaire : stocker un Span dans un champ est dangereux.
Erreur n°2 : retourner un Span depuis une méthode async. Il ne faut pas faire ça, parce que les méthodes async peuvent "sauter" sur le heap. Utilisez plutôt un tableau ou un autre type.
Erreur n°3 : oublier que les modifications via Span sont reflétées dans le tableau d'origine. Ça peut modifier des données "à l'extérieur" et conduire à des comportements imprévisibles.
GO TO FULL VERSION