C# 7.2 : à la découverte des deux nouvelles fonctionnalités que sont Span<T> et Memory<T>
Un billet de François DORIN

Le , par François DORIN, Responsable .NET & Magazine
Dans le billet du jour, nous allons partir à la découverte de nouvelles fonctionnalités introduites avec C# 7.2 : Span et Memory.

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
L'intérêt majeur de Span 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 à 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 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 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 et une List, 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, cela ne serait pas possible et nous aurions une erreur à la compilation due au fait que l'indexeur d'une List renvoie une structure, qui ne peut donc pas être modifiée directement. Par contre, Span 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 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 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 entre en jeu.

Memory
Comme Span, Memory permet de stocker une référence à un tableau, à un indice de départ et à une longueur.
Par contre, contrairement à Span, il n'est pas possible d'accéder directement à un élément en particulier. Pour cela, il est nécessaire de convertir un Memory en Span, qu'il est ensuite possible d'utiliser classiquement. Et cela tombe bien, car justement, Memory dispose d'une propriété nommée Span permettant de réaliser cette opération de conversion !

ReadOnlySpan
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. C'est comme Span sauf que les accès en écriture ne sont pas autorisés.

L'avantage, c'est que le ReadOnlySpan est compatible avec la classe String afin d'avoir un ReadOnlySpan. En effet, la classe string étant immutable, un string est non modifiable. C'est pourquoi la conversion d'un string en Span n'est pas possible.


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster un commentaire

Avatar de longbeach longbeach - Rédacteur https://www.developpez.com
le 16/04/2018 à 14:23
C'est quoi SIZE<T> ?
Avatar de Pyramidev Pyramidev - Membre expert https://www.developpez.com
le 16/04/2018 à 14:26
Remarque historique :
En C++, on a gsl::span qui est pareil que Span de C#, sauf qu'on n'est pas obligé de l'allouer dans la pile.
gsl::span vient des C++ Core Guidelines qui ont été co-écrites par Bjarne Stroustrup, le créateur du langage C++ et Herb Sutter, un développeur chez Microsoft.
Microsoft a implémenté la GSL, dont gsl::span.

Aujourd'hui, j'apprends que cette fonctionnalité se retrouve dans C#.
Avatar de richardc richardc - Membre à l'essai https://www.developpez.com
le 16/04/2018 à 15:49
Span<T> et Memory<T> sont des types, pas une fonctionnalité du langage non ?
Avatar de François DORIN François DORIN - Responsable .NET & Magazine https://www.developpez.com
le 16/04/2018 à 16:33
Citation Envoyé par longbeach Voir le message
C'est quoi SIZE<T> ?
Une grosse erreur de ma part sur le titre. C'est corrigé. Il fallait bien entendu lire Span<T>... Merci.

@Pyramidev, merci pour la note historique

Citation Envoyé par richardc Voir le message
Span<T> et Memory<T> sont des types, pas une fonctionnalité du langage non ?
Ce sont des types qui ont été introduits en même temps qu'une mise à jour du langage. De plus, le type Span<T> utilise une fonctionnalité qui n'était pas présente auparavant : les variables de type "référence sur une structure".
Avatar de callo callo - Membre expérimenté https://www.developpez.com
le 17/04/2018 à 11:20
Merci François pour ton travail.
Je pense qu'il y a une petite coquille . Au lieu de
Code : Sélectionner tout
subArray[1] = 0;
, ça devrait être
Code : Sélectionner tout
subSpan[1] = 0;
Avatar de François DORIN François DORIN - Responsable .NET & Magazine https://www.developpez.com
le 17/04/2018 à 11:29
@callo Merci. C'est corrigé
Avatar de tomlev tomlev - Rédacteur/Modérateur https://www.developpez.com
le 18/04/2018 à 9:43
Salut François,

Bon article, merci !

L'intérêt majeur de Span<T> est de permettre de manipuler un tableau ou un sous-ensemble de tableau
En fait ça ne se limite pas aux tableaux. Ça permet d'accéder à la mémoire sous n'importe quelle forme : tableau "classique", string, mémoire non managée, tableau sur la pile (stackalloc)... Du coup c'est une abstraction super utile puisqu'elle permet d'uniformiser les différents patterns d'accès à la mémoire.
Avatar de François DORIN François DORIN - Responsable .NET & Magazine https://www.developpez.com
le 18/04/2018 à 10:02
Citation Envoyé par tomlev Voir le message
Bon article, merci !
Merci

Citation Envoyé par tomlev Voir le message

En fait ça ne se limite pas aux tableaux. Ça permet d'accéder à la mémoire sous n'importe quelle forme : tableau "classique", string, mémoire non managée, tableau sur la pile (stackalloc)... Du coup c'est une abstraction super utile puisqu'elle permet d'uniformiser les différents patterns d'accès à la mémoire.
Tout à fait. Le seul requis est que la zone de mémoire accédée soit contiguë.

J'envisage de faire un article dessus plus poussé (quand j'aurais le temps, c'est toujours la même chose ^^). Mais je vais rajouter une note pour le préciser, tu as raison
Contacter le responsable de la rubrique Accueil