.NET-Referenztypen im Vergleich zu Werttypen. Teil 1

Lassen Sie uns zunächst über Referenztypen und Werttypen sprechen. Ich denke, die Leute verstehen die Unterschiede und Vorteile von beiden nicht wirklich. Sie sagen normalerweise, dass Referenztypen Inhalte auf dem Heap speichern und Werttypen Inhalte auf dem Stapel speichern, was falsch ist.


Lassen Sie uns die wirklichen Unterschiede diskutieren:


  • Ein Werttyp : Sein Wert ist eine gesamte Struktur . Der Wert eines Referenztyps ist eine Referenz auf ein Objekt. - Eine Struktur im Speicher: Werttypen enthalten nur die von Ihnen angegebenen Daten. Referenztypen enthalten auch zwei Systemfelder. Der erste speichert 'SyncBlockIndex', der zweite speichert die Informationen zu einem Typ, einschließlich der Informationen zu einer virtuellen Methodentabelle (VMT).
  • Referenztypen können Methoden haben, die bei der Vererbung überschrieben werden. Werttypen können nicht vererbt werden.
  • Sie sollten einer Instanz eines Referenztyps Speicherplatz auf dem Heap zuweisen. Ein Werttyp kann auf dem Stapel zugewiesen werden oder er wird Teil eines Referenztyps. Dies erhöht die Leistung einiger Algorithmen ausreichend.

Es gibt jedoch gemeinsame Merkmale:


  • Beide Unterklassen können den Objekttyp erben und dessen Vertreter werden.

Schauen wir uns die einzelnen Funktionen genauer an.


Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.

Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen github / sidristij / dotnetbook .


Schauen wir uns die einzelnen Funktionen genauer an.


Kopieren


Der Hauptunterschied zwischen den beiden Typen ist wie folgt:


  • Alle Variablen-, Klassen- oder Strukturfelder oder Methodenparameter, die einen Referenztyp annehmen, speichern eine Referenz auf einen Wert.
  • Alle Variablen-, Klassen- oder Strukturfelder oder Methodenparameter, die einen Werttyp annehmen, speichern jedoch genau einen Wert, dh eine gesamte Struktur.

Dies bedeutet, dass das Zuweisen oder Übergeben eines Parameters an eine Methode den Wert kopiert. Auch wenn Sie die Kopie ändern, bleibt das Original gleich. Wenn Sie jedoch Referenztypfelder ändern, wirkt sich dies auf alle Teile aus, die auf eine Instanz eines Typs verweisen. Schauen wir uns das an
Beispiel:


DateTime dt = DateTime.Now; // Here, we allocate space for DateTime variable when calling a method, // but it will contain zeros. Next, let's copy all // values of the Now property to dt variable DateTime dt2 = dt; // Here, we copy the value once again object obj = new object(); // Here, we create an object by allocating memory on the Small Object Heap, // and put a pointer to the object in obj variable object obj2 = obj; // Here, we copy a reference to this object. Finally, // we have one object and two references. 

Es scheint, dass diese Eigenschaft mehrdeutige Codekonstrukte wie das erzeugt
Codeänderung in Sammlungen:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create an array of such structures and initialize the Data field = 5 var array = new [] { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field array[0].Data = 4; // Let's check the value Console.WriteLine(array[0].Data); 

In diesem Code steckt ein kleiner Trick. Es sieht so aus, als würden wir zuerst die Strukturinstanz abrufen und dann dem Datenfeld der Kopie einen neuen Wert zuweisen. Dies bedeutet, dass wir beim Überprüfen des Werts erneut 5 sollten. Dies ist jedoch nicht der Fall. MSIL verfügt über eine separate Anweisung zum Festlegen der Werte von Feldern in den Strukturen eines Arrays, wodurch die Leistung erhöht wird. Der Code funktioniert wie vorgesehen: Das Programm funktioniert
Ausgabe 4 an eine Konsole.


Mal sehen, was passiert, wenn wir diesen Code ändern:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field list[0].Data = 4; // Let's check the value Console.WriteLine(list[0].Data); 

Die Kompilierung dieses Codes schlägt fehl, da Sie beim Schreiben der list[0].Data = 4 die Kopie der Struktur erhalten. Tatsächlich rufen Sie eine Instanzmethode vom Typ List<T> , die dem Zugriff durch einen Index zugrunde liegt. Es nimmt die Kopie einer Struktur aus einem internen Array ( List<T> speichert Daten in Arrays) und gibt diese Kopie von der Zugriffsmethode mithilfe eines Index an Sie zurück. Als Nächstes versuchen Sie, die Kopie zu ändern, die nicht weiter verwendet wird. Dieser Code ist sinnlos. Ein Compiler verbietet ein solches Verhalten, da er weiß, dass Benutzer Werttypen missbrauchen. Wir sollten dieses Beispiel folgendermaßen umschreiben:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field. Then, let's save it again. var copy = list[0]; copy.Data = 4; list[0] = copy; // Let's check the value Console.WriteLine(list[0].Data); 

Dieser Code ist trotz seiner offensichtlichen Redundanz korrekt. Das Programm wird
Ausgabe 4 an eine Konsole.


Das nächste Beispiel zeigt, was ich unter „der Wert einer Struktur ist ein
gesamte Struktur ”


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } int x = 5; PersonInfo person; int y = 6; // Variant 2 int x = 5; int Height; int Width; int HairColor; int y = 6; 

Beide Beispiele sind hinsichtlich der Datenposition im Speicher ähnlich, da der Wert der Struktur die gesamte Struktur ist. Es ordnet den Speicher für sich selbst zu, wo er ist.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } class Employee { public int x; public PersonInfo person; public int y; } // Variant 2 class Employee { public int x; public int Height; public int Width; public int HairColor; public int y; } 

Diese Beispiele ähneln sich auch hinsichtlich der Position der Elemente im Speicher, da die Struktur einen definierten Platz unter den Klassenfeldern einnimmt. Ich sage nicht, dass sie völlig ähnlich sind, da Sie Strukturfelder mit Strukturmethoden bedienen können.


Dies ist natürlich bei Referenztypen nicht der Fall. Eine Instanz selbst befindet sich auf dem nicht erreichbaren Heap für kleine Objekte (SOH) oder dem Heap für große Objekte (LOH). Ein Klassenfeld enthält nur den Wert eines Zeigers auf eine Instanz: eine 32- oder 64-Bit-Zahl.


Schauen wir uns das letzte Beispiel an, um das Problem zu schließen.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } void Method(int x, PersonInfo person, int y); // Variant 2 void Method(int x, int HairColor, int Width, int Height, int y); 

In Bezug auf den Speicher funktionieren beide Codevarianten auf ähnliche Weise, jedoch nicht in Bezug auf die Architektur. Es ist nicht nur ein Ersatz für eine variable Anzahl von Argumenten. Die Reihenfolge ändert sich, weil Methodenparameter nacheinander deklariert werden. Sie werden auf ähnliche Weise auf den Stapel gelegt.


Der Stapel wächst jedoch von höheren zu niedrigeren Adressen. Das heißt, die Reihenfolge, in der eine Struktur Stück für Stück geschoben wird, unterscheidet sich von der Reihenfolge, in der sie als Ganzes geschoben wird.


Überschreibbare Methoden und Vererbung


Der nächste große Unterschied zwischen den beiden Typen ist das Fehlen von virtuellen
Methodentabelle in Strukturen. Dies bedeutet, dass:


  1. Sie können virtuelle Methoden in Strukturen nicht beschreiben und überschreiben.
  2. Eine Struktur kann keine andere erben. Die einzige Möglichkeit, die Vererbung zu emulieren, besteht darin, eine Basistypstruktur in das erste Feld einzufügen. Die Felder einer "geerbten" Struktur folgen den Feldern einer "Basis" -Struktur und erzeugen eine logische Vererbung. Die Felder beider Strukturen fallen basierend auf dem Versatz zusammen.
  3. Sie können Strukturen an nicht verwalteten Code übergeben. Sie verlieren jedoch die Informationen über Methoden. Dies liegt daran, dass eine Struktur nur ein Speicherplatz ist, der mit Daten ohne Informationen zu einem Typ gefüllt ist. Sie können es ohne Änderungen an nicht verwaltete Methoden übergeben, die beispielsweise in C ++ geschrieben wurden.

Das Fehlen einer Tabelle mit virtuellen Methoden subtrahiert einen bestimmten Teil der Vererbungsmagie von den Strukturen, bietet ihnen jedoch andere Vorteile. Die erste besteht darin, dass wir Instanzen einer solchen Struktur an externe Umgebungen (außerhalb von .NET Framework) übergeben können. Denken Sie daran, dies ist nur eine Erinnerung
Reichweite! Wir können auch einen Speicherbereich von nicht verwaltetem Code nehmen und einen Typ in unsere Struktur umwandeln, um den Zugriff auf die Felder zu erleichtern. Sie können dies nicht mit Klassen tun, da diese zwei unzugängliche Felder haben. Dies sind SyncBlockIndex und eine Tabellenadresse für virtuelle Methoden. Wenn diese beiden Felder an nicht verwalteten Code übergeben werden, ist dies gefährlich. Mit einer virtuellen Methodentabelle kann man auf jeden Typ zugreifen und ihn ändern, um eine Anwendung anzugreifen.


Lassen Sie uns zeigen, dass es sich nur um einen Speicherbereich ohne zusätzliche Logik handelt.


 unsafe void Main() { int secret = 666; HeightHolder hh; hh.Height = 5; WidthHolder wh; unsafe { // This cast wouldn't work if structures had the information about a type. // The CLR would check a hierarchy before casting a type and if it didn't find WidthHolder, // it would output an InvalidCastException exception. But since a structure is a memory range, // you can interpret it as any kind of structure. wh = *(WidthHolder*)&hh; } Console.WriteLine("Width: " + wh.Width); Console.WriteLine("Secret:" + wh.Secret); } struct WidthHolder { public int Width; public int Secret; } struct HeightHolder { public int Height; } 

Hier führen wir die Operation aus, die bei starker Eingabe unmöglich ist. Wir wandeln einen Typ in einen anderen inkompatiblen Typ um, der ein zusätzliches Feld enthält. Wir führen eine zusätzliche Variable in die Main-Methode ein. Theoretisch ist sein Wert geheim. Der Beispielcode gibt jedoch den Wert einer Variablen aus, die in keiner der Strukturen innerhalb der Main() -Methode gefunden wurde. Sie könnten es als Sicherheitsverletzung betrachten, aber die Dinge sind nicht so einfach. Sie können nicht verwalteten Code in einem Programm nicht entfernen. Der Hauptgrund ist die Struktur des Thread-Stacks. Man kann damit auf nicht verwalteten Code zugreifen und mit lokalen Variablen spielen. Sie können Ihren Code vor diesen Angriffen schützen, indem Sie die Größe eines Stapelrahmens zufällig festlegen. Sie können auch die Informationen zum EBP Register löschen, um die Rückgabe eines EBP zu erschweren. Dies ist jedoch für uns jetzt nicht wichtig. Was uns an diesem Beispiel interessiert, ist das Folgende. Die "geheime" Variable steht vor der Definition der hh-Variablen und danach in der WidthHolder-Struktur (tatsächlich an verschiedenen Stellen). Warum haben wir seinen Wert leicht bekommen? Die Antwort ist, dass der Stapel von rechts nach links wächst. Die zuerst deklarierten Variablen haben viel höhere Adressen, und die später deklarierten Variablen haben niedrigere Adressen.


Das Verhalten beim Aufrufen von Instanzmethoden


Beide Datentypen haben eine weitere Funktion, die nicht klar erkennbar ist und die Struktur beider Typen erklären kann. Es befasst sich mit dem Aufrufen von Instanzmethoden.


 // The example with a reference type class FooClass { private int x; public void ChangeTo(int val) { x = val; } } // The example with a value type struct FooStruct { private int x; public void ChangeTo(int val) { x = val; } } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); klass.ChangeTo(10); strukt.ChangeTo(10); 

Logischerweise können wir entscheiden, dass die Methode einen kompilierten Körper hat. Mit anderen Worten, es gibt keine Instanz eines Typs, der über einen eigenen kompilierten Satz von Methoden verfügt, ähnlich wie die Sätze anderer Instanzen. Die aufgerufene Methode weiß jedoch, zu welcher Instanz sie gehört, da der Verweis auf die Instanz eines Typs der erste Parameter ist. Wir können unser Beispiel umschreiben und es wird mit dem identisch sein, was wir zuvor gesagt haben. Ich verwende kein Beispiel absichtlich mit virtuellen Methoden, da sie eine andere Prozedur haben.


 // An example with a reference type class FooClass { public int x; } // An example with a value type struct FooStruct { public int x; } public void ChangeTo(FooClass klass, int val) { klass.x = val; } public void ChangeTo(ref FooStruct strukt, int val) { strukt.x = val; } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); ChangeTo(klass, 10); ChangeTo(ref strukt, 10); 

Ich sollte die Verwendung des Schlüsselworts ref erklären. Wenn ich es nicht verwenden würde, würde ich anstelle des Originals eine Kopie der Struktur als Methodenparameter erhalten. Dann würde ich es ändern, aber das Original würde gleich bleiben. Ich müsste eine geänderte Kopie von einer Methode an einen Aufrufer zurückgeben (eine weitere Kopie), und der Aufrufer würde diesen Wert wieder in der Variablen speichern (eine weitere Kopie). Stattdessen erhält eine Instanzmethode einen Zeiger und verwendet ihn zum sofortigen Ändern des Originals. Die Verwendung eines Zeigers hat keinen Einfluss auf die Leistung, da Operationen auf Prozessorebene Zeiger verwenden. Ref ist ein Teil der C # -Welt, nicht mehr.


Die Fähigkeit, auf die Position von Elementen zu zeigen.


Sowohl Strukturen als auch Klassen haben eine andere Fähigkeit, auf den Versatz eines bestimmten Feldes in Bezug auf den Beginn einer Struktur im Speicher zu zeigen. Dies dient mehreren Zwecken:


  • mit externen APIs in der nicht verwalteten Welt zu arbeiten, ohne nicht verwendete Felder vor einem erforderlichen einfügen zu müssen;
  • um einen Compiler anzuweisen, ein Feld direkt am Anfang des Typs ( [FieldOffset(0)] ) zu [FieldOffset(0)] . Dies beschleunigt die Arbeit mit diesem Typ. Wenn es sich um ein häufig verwendetes Feld handelt, können wir die Leistung der Anwendung steigern. Dies gilt jedoch nur für Werttypen. Bei Referenztypen enthält das Feld mit einem Nullpunktversatz die Adresse einer virtuellen Methodentabelle, die 1 Maschinenwort enthält. Selbst wenn Sie das erste Feld einer Klasse adressieren, wird eine komplexe Adressierung (Adresse + Offset) verwendet. Dies liegt daran, dass das am häufigsten verwendete Klassenfeld die Adresse einer virtuellen Methodentabelle ist. Die Tabelle ist erforderlich, um alle virtuellen Methoden aufzurufen.
  • um mit einer Adresse auf mehrere Felder zu verweisen. In diesem Fall wird derselbe Wert als unterschiedliche Datentypen interpretiert. In C ++ wird dieser Datentyp als Union bezeichnet.
  • sich nicht die Mühe machen, etwas zu deklarieren: Ein Compiler ordnet Felder optimal zu. Daher kann die endgültige Reihenfolge der Felder unterschiedlich sein.

Allgemeine Bemerkungen


  • Auto : Die Laufzeitumgebung wählt automatisch einen Ort und eine Packung für alle Klassen- oder Strukturfelder aus. Die definierten Strukturen, die von einem Mitglied dieser Aufzählung markiert werden, können nicht in nicht verwalteten Code übertragen werden. Der Versuch, dies zu tun, führt zu einer Ausnahme.
  • Explizit : Ein Programmierer steuert explizit die genaue Position jedes Felds eines Typs mit dem FieldOffsetAttribute.
  • Sequenziell : Typelemente werden in einer sequentiellen Reihenfolge geliefert, die während des Typentwurfs definiert wird. Der StructLayoutAttribute.Pack-Wert eines Packungsschritts gibt deren Position an.

Verwenden von FieldOffset zum Überspringen nicht verwendeter Strukturfelder


Die Strukturen aus der nicht verwalteten Welt können reservierte Felder enthalten. Man kann sie in einer zukünftigen Version einer Bibliothek verwenden. In C / C ++ füllen wir diese Lücken, indem wir Felder hinzufügen, z. B. reserviert1, reserviert2, ... In .NET versetzen wir jedoch nur mit dem FieldOffsetAttribute-Attribut und [StructLayout(LayoutKind.Explicit)] an den Anfang eines Feldes.


 [StructLayout(LayoutKind.Explicit)] public struct SYSTEM_INFO { [FieldOffset(0)] public ulong OemId; // 92 bytes reserved [FieldOffset(100)] public ulong PageSize; [FieldOffset(108)] public ulong ActiveProcessorMask; [FieldOffset(116)] public ulong NumberOfProcessors; [FieldOffset(124)] public ulong ProcessorType; } 

Eine Lücke ist belegt, aber ungenutzter Raum. Die Struktur hat die Größe 132 und nicht 40 Bytes, wie es von Anfang an scheinen mag.


Union


Mit dem FieldOffsetAttribute können Sie den C / C ++ - Typ emulieren, der als Union bezeichnet wird. Es ermöglicht den Zugriff auf dieselben Daten wie Entitäten von
verschiedene Arten. Schauen wir uns das Beispiel an:


 // If we read the RGBA.Value, we will get an Int32 value accumulating all // other fields. // However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we // will get separate components of Int32. [StructLayout(LayoutKind.Explicit)] public struct RGBA { [FieldOffset(0)] public uint Value; [FieldOffset(0)] public byte R; [FieldOffset(1)] public byte G; [FieldOffset(2)] public byte B; [FieldOffset(3)] public byte Alpha; } 

Man könnte sagen, dass ein solches Verhalten nur für Werttypen möglich ist. Sie können es jedoch für Referenztypen simulieren, indem Sie eine Adresse verwenden, um zwei Referenztypen oder einen Referenztyp und einen Werttyp zu überlappen:


 class Program { public static void Main() { Union x = new Union(); x.Reference.Value = "Hello!"; Console.WriteLine(x.Value.Value); } [StructLayout(LayoutKind.Explicit)] public class Union { public Union() { Value = new Holder<IntPtr>(); Reference = new Holder<object>(); } [FieldOffset(0)] public Holder<IntPtr> Value; [FieldOffset(0)] public Holder<object> Reference; } public class Holder<T> { public T Value; } } 

Ich habe absichtlich einen generischen Typ zum Überlappen verwendet. Wenn ich es gewohnt bin
Überlappend würde dieser Typ beim Laden in eine Anwendungsdomäne die TypeLoadException verursachen. Theoretisch mag es wie eine Sicherheitsverletzung aussehen (insbesondere wenn es um Anwendungs- Plug-Ins geht ), aber wenn wir versuchen, diesen Code unter Verwendung einer geschützten Domäne TypeLoadException , erhalten wir dieselbe TypeLoadException .


Der Unterschied in der Zuordnung


Ein weiteres Merkmal, das beide Typen unterscheidet, ist die Speicherzuordnung für Objekte oder Strukturen. Die CLR muss über mehrere Dinge entscheiden, bevor sie einem Objekt Speicher zuweist. Wie groß ist ein Objekt? Ist es mehr oder weniger als 85K? Wenn es weniger ist, gibt es dann genügend freien Speicherplatz auf dem SOH, um dieses Objekt zuzuweisen? Wenn es mehr ist, aktiviert die CLR den Garbage Collector. Es geht durch ein Objektdiagramm, komprimiert die Objekte, indem es sie in den freien Raum verschiebt. Wenn auf dem SOH noch kein Speicherplatz vorhanden ist, wird die Zuweisung zusätzlicher virtueller Speicherseiten gestartet. Erst dann wird einem Objekt Speicherplatz zugewiesen, der aus dem Müll entfernt wird. Anschließend legt die CLR SyncBlockIndex und VirtualMethodsTable fest. Schließlich kehrt der Verweis auf ein Objekt zu einem Benutzer zurück.


Wenn ein zugewiesenes Objekt größer als 85 KB ist, wird es in den Heap für große Objekte (Large Objects Heap, LOH) verschoben. Dies ist bei großen Strings und Arrays der Fall. Hier müssen wir aus der Liste der nicht besetzten Bereiche den am besten geeigneten Speicherplatz finden oder einen neuen zuweisen. Es ist nicht schnell, aber wir werden sorgfältig mit Objekten dieser Größe umgehen. Wir werden hier auch nicht darüber sprechen.


Es gibt mehrere mögliche Szenarien für RefTypes:


  • RefType <85K, auf dem SOH ist Platz: schnelle Speicherzuweisung;
  • RefType <85K, der Speicherplatz auf dem SOH ist knapp: sehr langsame Speicherzuweisung;
  • RefType> 85K, langsame Speicherzuordnung.

Solche Operationen sind selten und können nicht mit ValTypes konkurrieren. Der Algorithmus der Speicherzuordnung für Werttypen existiert nicht. Die Speicherzuordnung für Werttypen kostet nichts. Das einzige, was beim Zuweisen von Speicher für diesen Typ passiert, ist das Setzen von Feldern auf Null. Mal sehen, warum dies passiert: 1. Wenn man eine Variable im Hauptteil einer Methode deklariert, ist die Zeit der Speicherzuweisung für eine Struktur nahe Null. Dies liegt daran, dass der Zeitpunkt der Zuweisung für lokale Variablen nicht von ihrer Anzahl abhängt. 2. Wenn ValTypes als Felder zugewiesen werden, erhöhen Reftypes die Größe der Felder. Ein Werttyp wird vollständig zugewiesen und wird zu seinem Teil. 3. Wie beim Kopieren tritt bei der Übergabe von ValTypes als Methodenparameter je nach Größe und Position eines Parameters ein Unterschied auf.


Dies dauert jedoch nicht länger als das Kopieren einer Variablen in eine andere.


Die Wahl zwischen einer Klasse oder einer Struktur


Lassen Sie uns die Vor- und Nachteile beider Typen diskutieren und ihre Verwendungsszenarien festlegen. Ein klassisches Prinzip besagt, dass wir einen Werttyp wählen sollten, wenn er nicht größer als 16 Byte ist, während seiner Lebensdauer unverändert bleibt und nicht vererbt wird. Die Auswahl des richtigen Typs bedeutet jedoch, ihn aus verschiedenen Perspektiven zu überprüfen, basierend auf Szenarien der zukünftigen Verwendung. Ich schlage drei Kriteriengruppen vor:


  • basierend auf der Typsystemarchitektur, in der Ihr Typ interagiert;
  • basierend auf Ihrem Ansatz als Systemprogrammierer bei der Auswahl eines Typs mit optimaler Leistung;
  • wenn es keine andere Wahl gibt.

Jedes entworfene Merkmal sollte seinen Zweck widerspiegeln. Dies betrifft nicht nur den Namen oder die Interaktionsschnittstelle (Methoden, Eigenschaften). Man kann architektonische Überlegungen verwenden, um zwischen Wert- und Referenztypen zu wählen. Lassen Sie uns überlegen, warum aus Sicht des Typsystemsystems eine Struktur und keine Klasse ausgewählt werden könnte.


  1. Wenn Ihr entworfener Typ für seinen Zustand agnostisch ist, bedeutet dies, dass sein Zustand einen Prozess widerspiegelt oder ein Wert von etwas ist. Mit anderen Worten, eine Instanz eines Typs ist von Natur aus konstant und unveränderlich. Wir können eine weitere Instanz eines Typs basierend auf dieser Konstante erstellen, indem wir einen Versatz angeben. Oder wir können eine neue Instanz erstellen, indem wir ihre Eigenschaften angeben. Wir dürfen es jedoch nicht ändern. Ich meine nicht, dass Struktur ein unveränderlicher Typ ist. Sie können die Feldwerte ändern. Darüber hinaus können Sie mit dem Parameter ref einen Verweis auf eine Struktur an eine Methode übergeben. Nach dem Beenden der Methode erhalten Sie geänderte Felder. Worüber ich hier spreche, ist der architektonische Sinn. Ich werde einige Beispiele geben.


    • DateTime ist eine Struktur, die das Konzept eines Zeitpunkts zusammenfasst. Es speichert diese Daten als Uint, bietet jedoch Zugriff auf separate Merkmale eines bestimmten Zeitpunkts: Jahr, Monat, Tag, Stunde, Minuten, Sekunden, Millisekunden und sogar Prozessorticks. Es ist jedoch unveränderlich und basiert auf dem, was es einschließt. Wir können keinen Moment in der Zeit ändern. Ich kann nicht in der nächsten Minute leben, als wäre es mein bester Geburtstag in der Kindheit. Wenn wir also einen Datentyp auswählen, können wir eine Klasse mit einer schreibgeschützten Schnittstelle auswählen, die für jede Änderung der Eigenschaften eine neue Instanz erzeugt. Oder wir können eine Struktur auswählen, die die Felder ihrer Instanzen ändern kann, aber nicht sollte: Ihr Wert ist die Beschreibung eines Zeitpunkts wie einer Zahl. Sie können nicht auf die Struktur einer Nummer zugreifen und diese ändern. Wenn Sie einen weiteren Moment erhalten möchten, der sich für einen Tag vom Original unterscheidet, erhalten Sie nur eine neue Instanz einer Struktur.
    • KeyValuePair<TKey, TValue> ist eine Struktur, die das Konzept eines verbundenen Schlüssel-Wert-Paares kapselt. Diese Struktur dient nur dazu, den Inhalt eines Wörterbuchs während der Aufzählung auszugeben. Aus architektonischer Sicht sind ein Schlüssel und ein Wert untrennbare Konzepte in Dictionary<T> . Im Inneren haben wir jedoch eine komplexe Struktur, in der ein Schlüssel getrennt von einem Wert liegt. Für einen Benutzer ist ein Schlüssel-Wert-Paar ein untrennbares Konzept in Bezug auf die Schnittstelle und die Bedeutung einer Datenstruktur. Es ist ein ganzer Wert . Wenn einem Schlüssel ein anderer Wert zugewiesen wird, ändert sich das gesamte Paar. Sie repräsentieren also eine einzelne Einheit. Dies macht eine Struktur in diesem Fall zu einer idealen Variante.

  2. Wenn Ihr entworfener Typ ein untrennbarer Bestandteil eines externen Typs ist, aber strukturell ein integraler Bestandteil ist. Das heißt, es ist falsch zu sagen, dass sich der externe Typ auf eine Instanz eines gekapselten Typs bezieht. Es ist jedoch richtig zu sagen, dass ein gekapselter Typ zusammen mit all seinen Eigenschaften Teil eines externen Typs ist. Dies ist nützlich, wenn Sie eine Struktur entwerfen, die Teil einer anderen Struktur ist.


    • Wenn wir beispielsweise eine Struktur eines Dateikopfs verwenden, ist es unangemessen, einen Verweis von einer Datei auf eine andere zu übergeben, z. B. eine Datei header.txt. Dies ist angemessen, wenn Sie ein Dokument in ein anderes einfügen, nicht durch Einbetten einer Datei, sondern durch Verwendung einer Referenz in einem Dateisystem. Ein gutes Beispiel sind Verknüpfungsdateien unter Windows. Wenn es sich jedoch um einen Dateikopf handelt (z. B. einen JPEG-Dateikopf mit Metadaten zu Bildgröße, Komprimierungsmethoden, Fotoparametern, GPS-Koordinaten usw.), sollten wir Strukturen zum Entwerfen von Typen zum Parsen des Kopfes verwenden. Wenn Sie alle Header in Strukturen beschreiben, erhalten Sie die gleiche Position der Felder im Speicher wie in einer Datei. Mit der einfachen unsicheren *(Header *)readedBuffer Transformation ohne Deserialisierung erhalten Sie vollständig gefüllte Datenstrukturen.


  1. Keines der beiden Beispiele zeigt die Vererbung von Verhalten. Sie zeigen, dass das Verhalten dieser Entitäten nicht geerbt werden muss. Sie sind in sich geschlossen. Wenn wir jedoch die Effektivität von Code berücksichtigen, werden wir die Wahl von einer anderen Seite sehen:
  2. Wenn wir strukturierte Daten aus nicht verwaltetem Code entnehmen müssen, sollten wir Strukturen auswählen. Wir können die Datenstruktur auch an eine unsichere Methode übergeben. Ein Referenztyp ist dafür überhaupt nicht geeignet.
  3. Eine Struktur ist Ihre Wahl, wenn ein Typ die Daten in Methodenaufrufen (als zurückgegebene Werte oder als Methodenparameter) übergibt und nicht von verschiedenen Stellen auf denselben Wert verwiesen werden muss. Das perfekte Beispiel sind Tupel. Wenn eine Methode mehrere Werte mit Tupeln zurückgibt, gibt sie ein ValueTuple zurück, das als Struktur deklariert ist. Die Methode reserviert keinen Speicherplatz auf dem Heap, verwendet jedoch den Stapel des Threads, wobei die Speicherzuweisung nichts kostet.
  4. Wenn Sie ein System entwerfen, das großen Datenverkehr mit Instanzen mit geringer Größe und Lebensdauer erzeugt, führt die Verwendung von Referenztypen entweder zu einem Objektpool oder, wenn kein Objektpool vorhanden ist, zu einer unkontrollierten Speicherbereinigung auf dem Heap. Einige Objekte werden zu älteren Generationen, was die Belastung des GC erhöht. Die Verwendung von Werttypen an solchen Stellen (wenn dies möglich ist) führt zu einer Leistungssteigerung, da nichts an die SOH weitergegeben wird. Dies verringert die Belastung des GC und der Algorithmus arbeitet schneller.

Auf der Grundlage meiner Aussagen finden Sie hier einige Ratschläge zur Verwendung von Strukturen:


  1. Bei der Auswahl von Sammlungen sollten Sie große Arrays vermeiden, in denen große Strukturen gespeichert sind. Dies schließt Datenstrukturen ein, die auf Arrays basieren. Dies kann zu einem Übergang zum Heap für große Objekte und seiner Fragmentierung führen. Es ist falsch zu glauben, dass unsere Struktur 4 Bytes benötigt, wenn sie 4 Felder vom Bytetyp hat. Wir sollten verstehen, dass in 32-Bit-Systemen jedes Strukturfeld an 4-Byte-Grenzen ausgerichtet ist (jedes Adressfeld sollte genau durch 4 geteilt werden) und in 64-Bit-Systemen an 8-Byte-Grenzen. Die Größe eines Arrays sollte von der Größe einer Struktur und einer Plattform abhängen, auf der ein Programm ausgeführt wird. In unserem Beispiel mit 4 Bytes - 85 KB / (von 4 bis 8 Bytes pro Feld * Anzahl der Felder = 4) abzüglich der Größe eines Array-Headers entspricht dies je nach Plattform etwa 2 600 Elementen pro Array (dies sollte abgerundet werden ) Das ist nicht sehr viel. Es schien, als könnten wir leicht eine magische Konstante von 20.000 Elementen erreichen
  2. Manchmal verwenden Sie eine große Struktur als Datenquelle und platzieren sie als Feld in einer Klasse, während eine Kopie repliziert wird, um tausend Instanzen zu erzeugen. Anschließend erweitern Sie jede Instanz einer Klasse um die Größe einer Struktur. Dies führt zu einer Schwellung der Generation Null und zum Übergang zur ersten und sogar zur zweiten Generation. Wenn die Instanzen einer Klasse eine kurze Lebensdauer haben und Sie glauben, dass der GC sie bei der Generation Null sammelt - für 1 ms, werden Sie enttäuscht sein. Sie sind bereits in der ersten und sogar zweiten Generation. Das macht den Unterschied. Wenn der GC 1 ms lang die Generation Null sammelt, werden die Generationen eins und zwei sehr langsam gesammelt, was zu einer Abnahme der Effizienz führt.
  3. Aus dem gleichen Grund sollten Sie vermeiden, große Strukturen durch eine Reihe von Methodenaufrufen zu leiten. Wenn sich alle Elemente gegenseitig aufrufen, beanspruchen diese Aufrufe mehr Speicherplatz auf dem Stapel und bringen Ihre Anwendung durch StackOverflowException zum Tode. Der nächste Grund ist die Leistung. Je mehr Kopien es gibt, desto langsamer funktioniert alles.

Aus diesem Grund ist die Auswahl eines Datentyps kein offensichtlicher Prozess. Dies kann häufig auf eine vorzeitige Optimierung hinweisen, die nicht empfohlen wird. Wenn Sie jedoch wissen, dass Ihre Situation unter die oben genannten Grundsätze fällt, können Sie leicht einen Werttyp auswählen.


Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.

Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen github / sidristij / dotnetbook .

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


All Articles