À partir de .NET Core 2.0 et .NET Framework 4.5, nous pouvons utiliser de nouveaux types de données: Span
et Memory
. Pour les utiliser, il vous suffit d'installer le package nuget System.Memory
:
PM> Install-Package System.Memory
Ces types de données sont remarquables parce que l'équipe CLR a fait un excellent travail pour implémenter leur prise en charge spéciale dans le code du compilateur JIT .NET Core 2.1+ en incorporant ces types de données directement dans le noyau. De quels types de données s'agit-il et pourquoi valent-elles un chapitre entier?
Si nous parlons de problèmes qui ont fait apparaître ces types, je devrais en nommer trois. Le premier est du code non managé.
Le langage et la plate-forme existent depuis de nombreuses années ainsi que les moyens de travailler avec du code non managé. Alors, pourquoi publier une autre API pour travailler avec du code non managé si la première existait essentiellement depuis de nombreuses années? Pour répondre à cette question, nous devons comprendre ce qui nous manquait auparavant.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository
github / sidristij / dotnetbook .
Les développeurs de la plateforme ont déjà essayé de faciliter l'utilisation de ressources non gérées pour nous. Ils ont implémenté des wrappers automatiques pour les méthodes importées et le marshaling qui fonctionne automatiquement dans la plupart des cas. Ici aussi appartient à stackalloc
, mentionné dans le chapitre sur une pile de threads. Cependant, à mon avis, les premiers développeurs C # sont venus du monde C ++ (mon cas), mais maintenant ils passent de langages plus avancés (je connais un développeur qui a déjà écrit en JavaScript). Cela signifie que les gens deviennent de plus en plus suspects envers le code non managé et les constructions C / C +, d'autant plus pour Assembler.
En conséquence, les projets contiennent de moins en moins de code dangereux et la confiance dans l'API de la plate-forme augmente de plus en plus. Il est facile de vérifier si nous recherchons des cas d'utilisation stackalloc
dans les référentiels publics - ils sont rares. Cependant, prenons tout code qui l'utilise:
Classe Interop.ReadDir
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs
unsafe { // s_readBufferSize is zero when the native implementation does not support reading into a buffer. byte* buffer = stackalloc byte[s_readBufferSize]; InternalDirectoryEntry temp; int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp); // We copy data into DirectoryEntry to ensure there are no dangling references. outputEntry = ret == 0 ? new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } : default(DirectoryEntry); return ret; }
Nous pouvons voir pourquoi il n'est pas populaire. Parcourez simplement ce code et demandez-vous si vous lui faites confiance. Je suppose que la réponse est «non». Ensuite, demandez-vous pourquoi. C'est évident: non seulement nous voyons le mot Dangerous
, ce qui suggère que quelque chose peut mal tourner, mais il y a le mot-clé unsafe
et l' byte* buffer = stackalloc byte[s_readBufferSize];
(spécifiquement - byte*
) qui changent notre attitude. C'est un déclencheur pour vous de penser: "N'y avait-il pas une autre façon de le faire"? Alors, approfondissons la psychanalyse: pourquoi pensez-vous ainsi? D'une part, nous utilisons des constructions de langage et la syntaxe proposée ici est loin, par exemple, de C ++ / CLI, qui permet tout (même l'insertion de code Assembleur pur). D'un autre côté, cette syntaxe semble inhabituelle.
Le deuxième problème auquel les développeurs pensaient implicitement ou explicitement est l'incompatibilité des types chaîne et char []. Bien que, logiquement, une chaîne soit un tableau de caractères, mais vous ne pouvez pas convertir une chaîne en char []: vous pouvez uniquement créer un nouvel objet et copier le contenu d'une chaîne dans un tableau. Cette incompatibilité est introduite pour optimiser les chaînes en termes de stockage (il n'y a pas de tableaux en lecture seule). Cependant, des problèmes apparaissent lorsque vous commencez à travailler avec des fichiers. Comment les lire? En tant que chaîne ou tableau? Si vous choisissez un tableau, vous ne pouvez pas utiliser certaines méthodes conçues pour fonctionner avec des chaînes. Et la lecture sous forme de chaîne? Cela peut être trop long. Si vous devez ensuite l'analyser, quel analyseur devez-vous choisir pour les types de données primitifs: vous ne voulez pas toujours les analyser manuellement (entiers, flottants, donnés dans différents formats). Nous avons beaucoup d'algorithmes éprouvés qui le font plus rapidement et plus efficacement, n'est-ce pas? Cependant, ces algorithmes fonctionnent souvent avec des chaînes qui ne contiennent rien d'autre qu'un type primitif lui-même. Il y a donc un dilemme.
Le troisième problème est que les données requises par un algorithme font rarement une tranche de données continue et solide dans une section d'un tableau lu à partir d'une source. Par exemple, dans le cas de fichiers ou de données lus à partir d'un socket, nous avons une partie de ceux déjà traités par un algorithme, suivis d'une partie de données qui doivent être traitées par notre méthode, puis de données non encore traitées. Idéalement, notre méthode ne veut que les données pour lesquelles cette méthode a été conçue. Par exemple, une méthode qui analyse des entiers ne sera pas satisfaite d'une chaîne contenant certains mots avec un nombre attendu quelque part parmi eux. Cette méthode veut un nombre et rien d'autre. Ou, si nous passons un tableau entier, il est nécessaire d'indiquer, par exemple, le décalage d'un nombre depuis le début du tableau.
int ParseInt(char[] input, int index) { while(char.IsDigit(input[index])) { // ... index++; } }
Cependant, cette approche est mauvaise, car cette méthode obtient des données inutiles. En d'autres termes, la méthode est appelée pour des contextes pour lesquels elle n'a pas été conçue et doit résoudre certaines tâches externes. C'est une mauvaise conception. Comment éviter ces problèmes? En option, nous pouvons utiliser le type ArraySegment<T>
qui peut donner accès à une section d'un tableau:
int ParseInt(IList<char>[] input) { while(char.IsDigit(input.Array[index])) { // ... index++; } } var arraySegment = new ArraySegment(array, from, length); var res = ParseInt((IList<char>)arraySegment);
Cependant, je pense que c'est trop à la fois en termes de logique et de diminution des performances. ArraySegment
est mal conçu et ralentit l'accès aux éléments 7 fois plus par rapport aux mêmes opérations effectuées avec un tableau.
Alors, comment pouvons-nous résoudre ces problèmes? Comment amener les développeurs à utiliser du code non managé et leur donner un outil unifié et rapide pour travailler avec des sources de données hétérogènes: tableaux, chaînes et mémoire non managée. Il était nécessaire de leur donner un sentiment de confiance qu'ils ne pouvaient pas faire une erreur sans le savoir. Il était nécessaire de leur donner un instrument qui ne diminue pas les types de données natives en termes de performances mais résout les problèmes répertoriés. Span<T>
types Span<T>
et Memory<T>
sont exactement ces instruments.
Span <T>, ReadOnlySpan <T>
Span
type Span
est un instrument permettant de travailler avec des données dans une section d'un tableau de données ou avec une sous-plage de ses valeurs. Comme dans le cas d'un tableau, il permet à la fois de lire et d'écrire sur les éléments de cette sous-gamme, mais avec une contrainte importante: vous obtenez ou créez un Span<T>
uniquement pour un travail temporaire avec un tableau, juste pour appeler un groupe de méthodes . Cependant, pour obtenir une compréhension générale, comparons les types de données pour lesquels Span
est conçu et examinons ses scénarios d'utilisation possibles.
Le premier type de données est un tableau habituel. Les tableaux fonctionnent avec Span
de la manière suivante:
var array = new [] {1,2,3,4,5,6}; var span = new Span<int>(array, 1, 3); var position = span.BinarySearch(3); Console.WriteLine(span[position]); // -> 3
Dans un premier temps, nous créons un tableau de données, comme le montre cet exemple. Ensuite, nous créons Span
(ou un sous-ensemble) qui fait référence au tableau et rend une plage de valeurs précédemment initialisée accessible au code qui utilise le tableau.
Nous voyons ici la première caractéristique de ce type de données, à savoir la possibilité de créer un certain contexte. Développons notre idée des contextes:
void Main() { var array = new [] {'1','2','3','4','5','6'}; var span = new Span<char>(array, 1, 3); if(TryParseInt32(span, out var res)) { Console.WriteLine(res); } else { Console.WriteLine("Failed to parse"); } } public bool TryParseInt32(Span<char> input, out int result) { result = 0; for (int i = 0; i < input.Length; i++) { if(input[i] < '0' || input[i] > '9') return false; result = result * 10 + ((int)input[i] - '0'); } return true; } ----- 234
Comme nous le voyons, Span<T>
fournit un accès abstrait à une plage de mémoire à la fois pour la lecture et l'écriture. Que nous apporte-t-il? Si nous nous souvenons à quoi d'autre nous pouvons utiliser Span
, nous penserons aux ressources et chaînes non gérées:
// Managed array var array = new[] { '1', '2', '3', '4', '5', '6' }; var arrSpan = new Span<char>(array, 1, 3); if (TryParseInt32(arrSpan, out var res1)) { Console.WriteLine(res1); } // String var srcString = "123456"; var strSpan = srcString.AsSpan(); if (TryParseInt32(strSpan, out var res2)) { Console.WriteLine(res2); } // void * Span<char> buf = stackalloc char[6]; buf[0] = '1'; buf[1] = '2'; buf[2] = '3'; buf[3] = '4'; buf[4] = '5'; buf[5] = '6'; if (TryParseInt32(buf, out var res3)) { Console.WriteLine(res3); } ----- 234 234 234
Cela signifie que Span<T>
est un outil pour unifier les façons de travailler avec la mémoire, à la fois gérée et non gérée. Il garantit la sécurité lors de l'utilisation de ces données lors de la collecte des ordures. C'est-à-dire que si les plages de mémoire avec des ressources non gérées commencent à se déplacer, ce sera sûr.
Cependant, devrions-nous être si excités? Pourrions-nous y parvenir plus tôt? Par exemple, dans le cas de tableaux gérés, cela ne fait aucun doute: il vous suffit d'envelopper un tableau dans une autre classe (par exemple, [ArraySegment] existant depuis longtemps ( https://referencesource.microsoft.com/#mscorlib/system/ arraysegment.cs, 31 )) donnant ainsi une interface similaire et c'est tout. De plus, vous pouvez faire de même avec des chaînes - elles ont les méthodes nécessaires. Encore une fois, il vous suffit d'encapsuler une chaîne du même type et de fournir des méthodes pour travailler avec. Cependant, pour stocker une chaîne, un tampon et un tableau dans un type, vous aurez beaucoup à faire avec la conservation des références à chaque variante possible dans une seule instance (avec une seule variante active, évidemment).
public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... }
Ou, en fonction de l'architecture, vous pouvez créer trois types qui implémentent une interface uniforme. Ainsi, il n'est pas possible de créer une interface uniforme entre ces types de données différente de Span<T>
et de conserver les performances maximales.
Ensuite, il y a une question de ce qui est ref struct
en ce qui concerne Span
? Ce sont exactement ces «structures qui n'existent que sur pile» dont on entend si souvent parler lors des entretiens d'embauche. Cela signifie que ce type de données peut être alloué uniquement sur la pile et ne peut pas aller au segment de mémoire. C'est pourquoi Span
, qui est une structure ref, est un type de données contextuelles qui permet le travail des méthodes mais pas celui des objets en mémoire. C'est sur cela que nous devons nous baser pour essayer de le comprendre.
Nous pouvons maintenant définir le type Span
et le type ReadOnlySpan
associé:
L'étendue est un type de données qui implémente une interface uniforme pour fonctionner avec des types hétérogènes de tableaux de données et permet de passer un sous-ensemble d'un tableau à une méthode de sorte que la vitesse d'accès au tableau d'origine soit constante et la plus élevée quelle que soit la profondeur de la contexte.
En effet, si nous avons un code comme
public void Method1(Span<byte> buffer) { buffer[0] = 0; Method2(buffer.Slice(1,2)); } Method2(Span<byte> buffer) { buffer[0] = 0; Method3(buffer.Slice(1,1)); } Method3(Span<byte> buffer) { buffer[0] = 0; }
la vitesse d'accès au tampon d'origine sera la plus élevée lorsque vous travaillez avec un pointeur géré et non un objet géré. Cela signifie que vous travaillez avec un type non sécurisé dans un wrapper managé, mais pas avec un type managé .NET.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository
github / sidristij / dotnetbook .