Comprendre les distinctions entre Span<byte>, ReadOnlySpan<byte> et byte[] est essentiel pour optimiser l'efficacité de la mémoire et les performances dans les applications .NET. Ces trois types manipulent des séquences d'octets, mais avec des mécanismes et des contraintes fondamentalement différents.
Nature et allocation mémoire
Le tableau byte[] est un type géré dont l'instance est allouée sur le tas (heap). Sa création et sa collecte par le ramasse-miettes (GC) ont un coût.
En revanche, Span<byte> et ReadOnlySpan<byte> sont des ref structs. Ils ne représentent pas de données allouées indépendamment mais une vue fenêtrée sur une région de mémoire existante, qui peut être un tableau, du mémoire épinglé (pinned) ou la pile (stack). Ils n'entraînent donc pas d'allocation sur le tas et réduisent la pression sur le GC. La différence principale entre les deux est l'immutabilité : ReadOnlySpan<byte> interdit toute modification via sa vue.
Comparaison des caractéristiques clés
byte[]: Conteneur propriétaire de données. Nécessaire pour les API d'E/S, la sérialisation ou la conservation à long terme. Sa taille est fixe après création.Span<byte>: Vue mutable et à découpage (slicing) sur une mémoire contiguë. Idéale pour le traitement de données intermédiaires, l'analyse de protocoles ou les transformations sans copie. Sa durée de vie est limitée à la pile.ReadOnlySpan<byte>: Vue immutable sur la mémoire. Garantit que la source des données ne sera pas altérée. Parfait pour les fonctoins de validation, de parsing ou d'interface de lecture seule.
Scénarios d'utilisation typiques
Utilisez un byte[] pour recevoir des données depuis un réseau, lire un fichier ou pour tout contexte où le tableau doit être stocké ou transmis à une API tierce.
Privilégiez Span<byte> lorsque vous devez extraire une sous-section d'un tampon (buffer) pour la traiter, par exemple pour décoder un entier ou comparer des signatures, sans allouer de nouveau tableau.
Adoptez ReadOnlySpan<byte> dans les signatures de méthodes pour indiqeur clairement que la méthode ne modifiera pas les données passées en entrée, améliorant ainsi la sécurité et la lisibilité.
Exemples de code
Utilisation de base d'un tableau d'octets
// Un tableau classique pour le stockage.
byte[] tampon = new byte[256];
tampon[0] = 0x48; // 'H'
Découpage et traitement performant avec Span
// Créer une fenêtre sur une partie du tableau.
Span<byte> segment = tampon.AsSpan(1, 4);
short valeur = BitConverter.ToInt16(segment); // Lecture sans copie.
Interface de lecture seule avec ReadOnlySpan
public static bool ValiderEnTete(ReadOnlySpan<byte> enTete)
{
// On peut lire, mais on ne peut pas écrire : enTete[0] = 0; serait une erreur.
return enTete.Length >= 4 && enTete[0] == 0x89;
}
Contraintes et bonnes pratiques
En tant que ref struct, Span et ReadOnlySpan ont des restrictions strictes :
- Ils ne peuvent pas être stockés dans des champs de classe, des propriétés, des closures lambda ou des itérateurs.
- Ils ne peuvent pas être capturés par des lambdas pour des tâches asynchrones (
async/await) ni passés àTask.Run, car ils risqueraient de devenir invalides (dangling). - Pour découper une portion, utilisez la méthode
Slice()plutôt que de créer un nouveau tableau. - Évitez l'appel à
.ToArray()sur unSpanqui n'y est pas contraint, car cela annulerait le bénéfice de l'allocation zéro.
Choisir le bon type
Choisissez byte[] lorsque la donnée a une durée de vie indépendante ou doit être passée à des API traditionnelles.
Optez pour Span<byte> pour une manipulation de données locale et performante, sans allocation.
Préférez ReadOnlySpan<byte> pour définir des contrats d'API clairs et garantir l'intégrité des données en entrée.