[DotNetBook] Span: Neuer .NET-Datentyp

Mit diesem Artikel veröffentliche ich weiterhin eine Reihe von Artikeln, deren Ergebnis ein Buch über die Arbeit von .NET CLR und .NET im Allgemeinen sein wird (ca. 200 Seiten des Buches sind bereits fertig, daher willkommen am Ende des Artikels für Links).


Sowohl die Sprache als auch die Plattform gibt es schon seit vielen Jahren: und die ganze Zeit über gab es viele Tools für die Arbeit mit nicht verwaltetem Code. Warum kommt nun die nächste API für die Arbeit mit nicht verwaltetem Code heraus, wenn sie tatsächlich seit vielen, vielen Jahren existiert? Um diese Frage zu beantworten, reicht es zu verstehen, was vorher fehlte.


Die Entwickler der Plattform haben versucht, uns dabei zu helfen, den Entwicklungsalltag mit nicht verwalteten Ressourcen aufzuhellen: Dies sind automatische Wrapper für importierte Methoden. Und Marshalling, das in den meisten Fällen automatisch funktioniert. Dies ist auch eine stackallloc Anweisung, die im Kapitel über den Thread-Stack erläutert wird. Wenn jedoch frühe Entwickler, die C # verwenden, aus der C ++ - Welt stammen (wie ich), kommen sie jetzt aus höheren Sprachen (zum Beispiel kenne ich einen Entwickler, der aus JavaScript stammt). Was bedeutet das? Dies bedeutet, dass Menschen zunehmend misstrauisch gegenüber nicht verwalteten Ressourcen und Konstrukten sind, die C / C ++ und vor allem Assembler ähneln.


Hinweis


Das auf Habré veröffentlichte Kapitel ist nicht aktualisiert und wahrscheinlich bereits etwas veraltet. Wenden Sie sich daher für einen neueren Text dem Original zu:



Infolge einer solchen Einstellung gibt es in Projekten immer weniger Inhalte von unsicherem Code und immer mehr Vertrauen in die API der Plattform selbst. Dies lässt sich leicht anhand der Verwendung des stackalloc Konstrukts in offenen Repositorys überprüfen: Es ist vernachlässigbar. Aber wenn Sie einen Code nehmen, der ihn verwendet:


Klasse 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; } 

Der Grund für die Unbeliebtheit wird deutlich. Schauen Sie, ohne den Code zu lesen, und beantworten Sie eine Frage für sich selbst: Vertrauen Sie ihm? Ich kann davon ausgehen, dass die Antwort nein ist. Dann antworte dem anderen: warum? Die Antwort wird offensichtlich sein: Zusätzlich zum Wort Dangerous , das irgendwie darauf hindeutet, dass etwas schief gehen könnte, ist der zweite Faktor, der unsere Einstellung beeinflusst, das byte* buffer = stackalloc byte[s_readBufferSize]; und insbesondere byte* . Diese Aufzeichnung ist ein Auslöser für jeden, so dass der Gedanke in meinem Kopf auftaucht: "Was könnte nicht anders gemacht werden oder was?". Dann schauen wir uns die Psychoanalyse etwas genauer an: Warum könnte so ein Gedanke entstehen? Einerseits verwenden wir Sprachkonstrukte, und die hier vorgeschlagene Syntax ist weit entfernt von beispielsweise C ++ / CLI, mit der Sie überhaupt etwas tun können (einschließlich Einfügungen in reinen Assembler), und andererseits sieht sie ungewöhnlich aus.


Was ist die Frage? Wie können Entwickler wieder in den Kern des nicht verwalteten Codes zurückkehren? Es ist notwendig, ihnen ein Gefühl der Ruhe zu geben, damit sie nicht versehentlich aus Unwissenheit einen Fehler machen können. Warum werden die Span<T> und Memory<T> eingeführt?


Span [T], ReadOnlySpan [T]


Der Span Typ repräsentiert einen Teil eines bestimmten Datenarrays, einen Teilbereich seiner Werte. Gleichzeitig können Sie wie im Fall eines Arrays mit Elementen dieses Bereichs sowohl zum Schreiben als auch zum Lesen arbeiten. Zum Übertakten und zum allgemeinen Verständnis vergleichen wir jedoch die Datentypen, für die eine Implementierung des Span Typs vorgenommen wird, und untersuchen die möglichen Zwecke seiner Einführung.


Der erste Datentyp, über den Sie sprechen möchten, ist ein reguläres Array. Bei Arrays sieht die Arbeit mit Span folgendermaßen aus:


  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 

Wie wir in diesem Beispiel sehen, erstellen wir zunächst ein bestimmtes Datenarray. Danach erstellen wir einen Span (oder eine Teilmenge), die es dem Code unter Bezugnahme auf das Array selbst ermöglicht, nur den Wertebereich zu verwenden, der während der Initialisierung angegeben wurde.


Hier sehen wir die erste Eigenschaft dieses Datentyps: Sie erstellt einen Kontext. Lassen Sie uns unsere Idee mit Kontexten entwickeln:


 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 

Wie wir sehen können, führt Span<T> eine Abstraktion des Zugriffs auf ein bestimmtes Gedächtnis ein, sowohl zum Lesen als auch zum Schreiben. Was gibt uns das? Wenn wir uns daran erinnern, woraus Span sonst noch gemacht werden kann, erinnern wir uns sowohl an nicht verwaltete Ressourcen als auch an Zeilen:


 // 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().Slice(1, 3); 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.Slice(1, 3), out var res3)) { Console.WriteLine(res3); } ----- 234 234 234 

Das heißt, es stellt sich heraus, dass Span<T> ein Mittel zur Vereinheitlichung beim Arbeiten mit Speicher ist: verwaltet und nicht verwaltet, was die Sicherheit beim Arbeiten mit dieser Art von Daten während der Garbage Collection garantiert: Wenn sich Speicherbereiche mit verwalteten Arrays bewegen, dann für es wird für uns sicher sein.


Lohnt es sich jedoch, sich so sehr zu freuen? Könnte dies alles schon einmal erreicht worden sein? Wenn wir zum Beispiel über verwaltete Arrays sprechen, gibt es keinen Zweifel: Wickeln Sie das Array einfach in eine andere Klasse ein und stellen Sie eine ähnliche Schnittstelle bereit. Fertig. Darüber hinaus kann eine ähnliche Operation mit Zeichenfolgen durchgeführt werden: Sie verfügen über die erforderlichen Methoden. Wiederum wickeln Sie den String einfach in genau den gleichen Typ und stellen Sie Methoden für die Arbeit damit bereit. Eine andere Sache ist, dass Sie, um eine Zeichenfolge, einen Puffer oder ein Array in einem Typ zu speichern, viel basteln müssen, indem Sie Links zu jeder der möglichen Optionen in einer einzigen Kopie speichern (natürlich ist nur eine aktiv):


 public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... } 

Wenn Sie von der Architektur ausgehen, führen Sie drei Typen aus, die eine einzelne Schnittstelle erben. Es stellt sich heraus, dass es keine andere Möglichkeit als Span<T> gibt, um das Tool zu einer einheitlichen Schnittstelle zwischen diesen verwalteten Datentypen zu machen und gleichzeitig die maximale Leistung aufrechtzuerhalten.


Was ist eine ref struct in Bezug auf Span , um die Diskussion fortzusetzen? Dies sind genau die „Strukturen, sie sind nur auf dem Stapel“, von denen wir so oft in Interviews hören. Dies bedeutet, dass dieser Datentyp nur den Stapel durchlaufen kann und nicht das Recht hat, zum Heap zu wechseln. Daher ist Span als Referenzstruktur ein Kontextdatentyp, der Methoden, jedoch keine Objekte im Speicher bereitstellt. Aus diesem Grund müssen wir nach seinem Verständnis fortfahren.


Von hier aus können wir eine Definition des Span-Typs und des damit verbundenen schreibgeschützten Typs ReadOnlySpan formulieren:


Span ist ein Datentyp, der eine einzige Schnittstelle für die Arbeit mit heterogenen Typen von Datenarrays sowie die Möglichkeit bietet, eine Teilmenge dieses Arrays auf eine andere Methode zu übertragen, sodass die Zugriffsgeschwindigkeit auf das ursprüngliche Array unabhängig von der Kontexttiefe konstant und so hoch wie möglich ist.

Und wirklich: Wenn wir so etwas wie diesen Code haben:


 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; } 

Dann ist die Zugriffsgeschwindigkeit auf den Quellpuffer so hoch wie möglich: Sie arbeiten nicht mit einem verwalteten Objekt, sondern mit einem verwalteten Zeiger. Das heißt, Nicht mit einem von .NET verwalteten Typ, sondern mit einem unsicheren Typ, der in eine verwaltete Shell eingeschlossen ist.


Span [T] anhand von Beispielen


Eine Person ist so arrangiert, dass oft, bis sie eine bestimmte Erfahrung erhält, ein endgültiges Verständnis dafür, warum ein Werkzeug benötigt wird, oft nicht kommt. Da wir also einige Erfahrung benötigen, wenden wir uns Beispielen zu.


ValueStringBuilder


Eines der algorithmisch interessantesten Beispiele ist der ValueStringBuilder Typ, der irgendwo im Darm von mscorlib und aus irgendeinem Grund wie viele andere interessante Datentypen mit dem internal Modifikator gekennzeichnet ist. Wenn es sich also nicht um das Studium des mscorlib-Quellcodes handelt, werden wir über eine solch wunderbare Optimierungsmethode sprechen würde es nie erfahren.


Was ist das Haupt-Minus des StringBuilder-Systemtyps? Dies ist natürlich das Wesentliche: Sowohl er selbst als auch das, worauf er basiert (und dies ist eine Reihe von char[] -Zeichen), sind Referenztypen. Und das bedeutet mindestens zwei Dinge: Wir laden immer noch (wenn auch ein wenig) eine Menge und die zweite - wir erhöhen die Wahrscheinlichkeit, dass Prozessor-Caches übersehen werden.


Eine andere Frage, die ich für StringBuilder hatte, war die Bildung kleiner Strings. Das heißt, Wenn die Ergebniszeile "Zahn geben" kurz ist: Zum Beispiel weniger als 100 Zeichen. Bei relativ kurzer Formatierung treten Leistungsprobleme auf:


  $"{x} is in range [{min};{max}]" 

Wie viel schlimmer ist dieser Datensatz als die manuelle Generierung über StringBuilder? Die Antwort ist alles andere als immer offensichtlich: Alles hängt vom Ort der Bildung ab: Wie oft wird diese Methode aufgerufen? Schließlich weist das erste string.Format dem internen StringBuilder Speicher zu, der ein Array von Zeichen (SourceString.Length + args.Length * 8) erstellt. Wenn sich bei der Bildung des Arrays herausstellt, dass die Länge nicht erraten wurde, wird ein weiterer StringBuilder erstellt, um die Fortsetzung zu bilden. wodurch eine einfach verbundene Liste gebildet wird. Infolgedessen muss die generierte Zeile zurückgegeben werden. Dies ist eine weitere Kopie. Verschwenden und Verschwenden. Wenn wir es nun schaffen würden, das erste Array des zu bildenden Strings auf dem Haufen zu platzieren, wäre es wunderbar: Wir würden definitiv ein Problem loswerden.


Schauen Sie sich den Typ aus dem Darm von mscorlib :


Klasse ValueStringBuilder
/ src / mscorlib / shared / System / Text / ValueStringBuilder


  internal ref struct ValueStringBuilder { //           private char[] _arrayToReturnToPool; //     private Span<char> _chars; private int _pos; //    ,       public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } //   -       public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } //       //     :   public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } //   ,     //         //            //           [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } //  :       private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); } 

Diese Klasse ähnelt in ihrer Funktionalität ihrem älteren Bruder StringBuilder , hat jedoch eine interessante und sehr wichtige Funktion: Es handelt sich um einen bedeutenden Typ. Das heißt, vollständig nach Wert gespeichert und übertragen. Der neueste Modifikator für den ref Typ, der der Signatur der Typdeklaration zugewiesen ist, weist darauf hin, dass dieser Typ eine zusätzliche Einschränkung aufweist: Er darf nur auf dem Stapel sein. Das heißt, Die Ausgabe seiner Instanzen in die Klassenfelder führt zu einem Fehler. Warum all diese Kniebeugen? Um diese Frage zu beantworten, schauen Sie sich einfach die StringBuilder Klasse an, deren Kern wir gerade beschrieben haben:


Klasse StringBuilder /src/mscorlib/src/System/Text/StringBuilder.cs


 public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16; 

StringBuilder ist eine Klasse, in der eine Verknüpfung zu einem Array von Zeichen besteht. Das heißt, Wenn Sie es erstellen, werden tatsächlich mindestens zwei Objekte erstellt: der StringBuilder selbst und ein Array von Zeichen mit mindestens 16 Zeichen (übrigens ist es deshalb so wichtig, die geschätzte Länge des Strings anzugeben: Bei seiner Konstruktion wird eine einfach verbundene Liste von Arrays mit 16 Zeichen erstellt. ) Was bedeutet dies im Zusammenhang mit unserer Konversation über den Typ ValueStringBuilder: Kapazität fehlt standardmäßig, weil Es nimmt Speicher von außen in Anspruch, ist selbst ein bedeutender Typ und zwingt den Benutzer, einen Puffer für Zeichen auf dem Stapel zuzuweisen. Infolgedessen wird die gesamte Typinstanz zusammen mit ihrem Inhalt auf den Stapel verschoben, und das Optimierungsproblem wird hier behoben. Keine Speicherzuordnung auf dem Heap? Kein Problem mit Leistungseinbußen auf dem Haufen. Aber Sie sagen mir: Warum nicht immer ValueStringBuilder (oder seine selbstgeschriebene Version: Ist es intern und für uns nicht zugänglich) verwenden? Die Antwort lautet: Sie müssen sich das Problem ansehen, das Sie lösen. Wird die resultierende Zeichenfolge eine bekannte Größe haben? Wird es ein bestimmtes bekanntes Maximum an Länge haben? Wenn die Antwort Ja lautet und die Zeichenfolgengröße einige vernünftige Grenzen nicht überschreitet, können Sie eine aussagekräftige Version von StringBuilder verwenden. Wenn wir sonst lange Schlangen erwarten, wechseln wir zur regulären Version.


ValueListBuilder


Der zweite Datentyp, den ich besonders erwähnen möchte, ist der ValueListBuilder Typ. Es wurde für Situationen erstellt, in denen es erforderlich ist, für kurze Zeit eine Sammlung von Elementen zu erstellen und diese sofort einem Algorithmus zur Verarbeitung zu übergeben.


Zustimmen: Die Aufgabe ist der ValueStringBuilder Aufgabe sehr ähnlich. Ja, und es wurde auf sehr ähnliche Weise gelöst:


Datei ValueListBuilder.cs


Um es ganz klar auszudrücken, solche Situationen sind ziemlich häufig. Bevor wir diese Frage jedoch auf andere Weise gelöst haben: Wir haben eine List , sie mit Daten gefüllt und den Link verloren. Wenn die Methode häufig genug aufgerufen wird, tritt eine traurige Situation auf: Viele Instanzen der List Klasse hängen auf dem Heap, und mit ihnen hängen die ihnen zugeordneten Arrays auf dem Heap. Jetzt ist dieses Problem behoben: Es werden keine zusätzlichen Objekte erstellt. Wie im Fall von ValueStringBuilder wurde es jedoch nur für Microsoft-Programmierer gelöst: Die Klasse verfügt über einen internal Modifikator.


Nutzungsbedingungen


Um die Essenz des neuen Datentyps endlich zu verstehen, müssen Sie damit „herumspielen“, indem Sie ein paar Dinge oder besser mehr Methoden schreiben, die ihn verwenden. Die Grundregeln können jetzt jedoch gelernt werden:


  • Wenn Ihre Methode einen eingehenden Datensatz verarbeitet, ohne dessen Größe zu ändern, können Sie versuchen, beim Span Typ anzuhalten. Wenn dieser Puffer nicht ReadOnlySpan Typ ReadOnlySpan .
  • Wenn Ihre Methode mit Zeichenfolgen arbeitet, Statistiken berechnet oder eine Zeichenfolge analysiert, muss Ihre Methode ReadOnlySpan<char> akzeptieren. Es ist verpflichtet: Dies ist eine neue Regel. Wenn Sie einen String akzeptieren, zwingen Sie schließlich jemanden, einen Teilstring für Sie zu erstellen
  • Wenn Sie im Rahmen der Arbeit der Methode ein relativ kurzes Array mit Daten (z. B. maximal 10 KB) erstellen müssen, können Sie ein solches Array einfach mit Span<TType> buf = stackalloc TType[size] . Natürlich sollte TType nur ein aussagekräftiger Typ sein, da stackalloc funktioniert nur mit aussagekräftigen Typen.

In anderen Fällen lohnt es sich, entweder den Memory oder die Verwendung klassischer Datentypen genauer zu betrachten.


Wie Span funktioniert


Außerdem möchte ich darüber sprechen, wie Span funktioniert und was daran so bemerkenswert ist. Und es gibt etwas zu besprechen: Der Datentyp selbst ist in zwei Versionen unterteilt: für .NET Core 2.0+ und für alle anderen.


Span.Fast.cs, .NET Core 2.0- Datei


 public readonly ref partial struct Span<T> { ///    .NET    internal readonly ByReference<T> _pointer; ///      private readonly int _length; // ... } 

Datei ??? [dekompiliert]


 public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... } 

Die Sache ist, dass das große .NET Framework und .NET Core 1. * keinen speziell modifizierten Garbage Collector haben (im Gegensatz zur Version von .NET Core 2.0+) und daher gezwungen sind, einen zusätzlichen Zeiger entlang zu ziehen: an den Anfang des Puffers, mit dem Arbeit. Das heißt, es stellt sich heraus, dass Span intern mit verwalteten Objekten der .NET-Plattform als nicht verwaltet arbeitet. Schauen Sie sich die Innenseiten der zweiten Version der Struktur an: Es gibt drei Felder. Das erste Feld ist eine Referenz auf das verwaltete Objekt. Der zweite ist der Versatz vom Anfang dieses Objekts in Bytes, um den Anfang des Datenpuffers zu erhalten (in Zeilen ist es ein Puffer mit Zeichen, in Arrays ein Puffer mit Arraydaten). Und schließlich ist das dritte Feld die Anzahl der Elemente dieses Puffers, die nacheinander gestapelt sind.


Nehmen Sie zum Beispiel den Span Job für Strings:


Datei coreclr :: src / System.Private.CoreLib / shared / System / MemoryExtensions.Fast.cs


 public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); } 

Wobei string.GetRawStringData() wie folgt lautet:


Felddefinitionsdatei coreclr :: src / System.Private.CoreLib / src / System / String.CoreCLR.cs


GetRawStringData-Definitionsdatei coreclr :: src / System.Private.CoreLib / shared / System / String.cs


 public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; } 

Das heißt, Es stellt sich heraus, dass die Methode direkt innerhalb der Linie verläuft und die ref char es Ihnen ermöglicht, die nicht verwaltete GC-Verbindung innerhalb der Linie zu verfolgen und sie während des GC-Vorgangs zusammen mit der Linie zu verschieben.


Die gleiche Geschichte passiert mit Arrays: Wenn Span erstellt wird, berechnet ein Code in der JIT den Versatz des Anfangs der Array-Daten und initialisiert den Span diesem Versatz. Und wie man Offsets für Strings und Arrays berechnet, haben wir im Kapitel über die Struktur von Objekten im Speicher gelernt.


Spanne [T] als Rückgabewert


, Span , , . :


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; } 

. , :


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; } 

. , , , .


, , , , . , . , , , x[0.99] .


, , , , : CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope : , , , .



Span<T> , . , use cases .


Link zum ganzen Buch



Source: https://habr.com/ru/post/de418911/


All Articles