L'objectif de ces deux nouvelles structures est de faciliter la manipulation de données, par exemple sous forme de tableau, et ceci de manière performante. Mais cela ne se limite pas aux tableaux ! Il peut s'agir d'un buffer, d'une string, de mémoire non managée, etc... Il s'agit donc d'une abstraction très intéressante pour la manipulation des données. Le seul prérequis : que les données soient contiguës en mémoire.
Span<T>
L'intérêt majeur de Span<T> est de permettre de manipuler un tableau ou un sous-ensemble de tableau de manière performante, car :
- la structure est allouée sur la pile (donc allocation et déallocation très rapide) ;
- aucune copie du tableau initiale n'est réalisée.
Prenons ce petit exemple :
Code C# : | Sélectionner tout |
1 2 3 4 | int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; Span<int> span = array; Span<int> subSpan = span.Slice(4, 3); |
Dans un premier temps, on instancie un tableau. Cela sera la base pour la suite.
Ensuite, on crée un Span<T> à partir de ce tableau. On peut noter que la création est aisée grâce à une conversion implicite.
Il est ensuite facile de créer un sous-tableau. Ici, la variable subSpan est un tableau de 3 éléments débutant à partir de l'élément situé à l'index 4 (soit 5, 6, 7).
L'avantage donc, est que ce nouveau « tableau » n'a pas nécessité d'allocation ! Et il y a même encore plus fort.
Code C# : | Sélectionner tout |
subSpan[1] = 0;
Ici, on modifie le second élément de notre sous-tableau, pour l'initialiser à 0. Donc maintenant, le sous-tableau contient les éléments 5, 0, 7.
Mais si on regarde le tableau initial, on constate qu'il est également modifié, preuve qu'il n'y a pas de copie !
Comment ça marche ? En fait, très simplement. Une structure Span<T> contient une référence au tableau initial, à l'index de départ et à la taille du tableau. Ensuite, les méthodes de la structure font le nécessaire pour déterminer l'index dans le tableau initial pour les différents accès.
Et ce n'est pas fini ! Span<T> utilise une nouvelle notion introduite avec C# 7.2 : les références sur les structures. Dit comme cela, cela reste un peu flou. Il faut se souvenir que les structures ne sont modifiables qu'à travers une variable. Il n'est pas possible de modifier directement une structure retournée par une fonction par exemple. Donnons un exemple plus concret, en abandonnant notre tableau d'entiers pour passer sur un tableau de structures :
Code C# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public struct MyStruct { public int Value; } ... MyStruct[] array = { new MyStruct() { Value = 1 }, new MyStruct() { Value = 2 }, new MyStruct() { Value = 3 } }; Span<MyStruct> span = array; List<MyStruct> list = new List<MyStruct>(array); span[1].Value = 42; list[1].Value = 42; // Ne compile pas |
Ici, à partir d'un tableau, j'ai initialisé un Span<T> et une List<T>, afin de bien mettre en évidence l'apport lié à l'usage des références sur les structures. Grâce à cela, il est possible de modifier directement une valeur, comme si c'était une variable. Si nous avions une List<T>, cela ne serait pas possible et nous aurions une erreur à la compilation due au fait que l'indexeur d'une List<T> renvoie une structure, qui ne peut donc pas être modifiée directement. Par contre, Span<T> renvoie non pas une structure, mais une référence à la structure, qui peut donc être directement modifiée !
Mais du coup, cela impose quand même quelques limitations. En effet, afin de garder une gestion de la mémoire performante, une instance de Span<T> ne peut être stockée que sur la pile et non sur le tas. De ce fait, il n'est pas possible d'utiliser un Span<T> en tant qu'attribut d'une classe ou dans une expression lambda par exemple.
Dans la plupart des cas, cela n'est pas un véritable souci, car cette structure permet surtout d'optimiser des algorithmes via une gestion performante des sous-tableaux.
Mais il peut y avoir des cas où cela serait utile, notamment dans le cas de procédure asynchrone. Et c'est là que Memory<T> entre en jeu.
Memory<T>
Comme Span<T>, Memory<T> permet de stocker une référence à un tableau, à un indice de départ et à une longueur.
Par contre, contrairement à Span<T>, il n'est pas possible d'accéder directement à un élément en particulier. Pour cela, il est nécessaire de convertir un Memory<T> en Span<T>, qu'il est ensuite possible d'utiliser classiquement. Et cela tombe bien, car justement, Memory<T> dispose d'une propriété nommée Span permettant de réaliser cette opération de conversion !
ReadOnlySpan<T>
Dans le cas où il n'est pas nécessaire de modifier le tableau initial, mais uniquement d'y accéder, il est possible d'utiliser un ReadOnlySpan<T>. C'est comme Span<T> sauf que les accès en écriture ne sont pas autorisés.
L'avantage, c'est que le ReadOnlySpan<T> est compatible avec la classe String afin d'avoir un ReadOnlySpan<char>. En effet, la classe string étant immutable, un string est non modifiable. C'est pourquoi la conversion d'un string en Span<char> n'est pas possible.