.NET-Referenztypen im Vergleich zu Werttypen. Teil 2


Der Objektbasistyp und die Implementierung von Schnittstellen. Boxen


Es scheint, wir sind durch die Hölle und das Hochwasser gekommen und können jedes Interview festhalten, auch das für das .NET CLR-Team. Gehen wir jedoch nicht zu microsoft.com und suchen nach offenen Stellen. Jetzt müssen wir verstehen, wie Werttypen ein Objekt erben, wenn sie weder einen Verweis auf SyncBlockIndex noch einen Zeiger auf eine virtuelle Methodentabelle enthalten. Dies wird unser Typensystem vollständig erklären und alle Teile eines Puzzles werden ihren Platz finden. Wir brauchen jedoch mehr als einen Satz.


Erinnern wir uns nun noch einmal daran, wie Werttypen im Speicher zugewiesen werden. Sie bekommen den Ort in Erinnerung genau dort, wo sie sind. Referenztypen werden auf dem Haufen kleiner und großer Objekte zugeordnet. Sie geben immer einen Verweis auf die Stelle auf dem Haufen an, an der sich das Objekt befindet. Jeder Werttyp verfügt über Methoden wie ToString, Equals und GetHashCode. Sie sind virtuell und überschreibbar, erlauben jedoch nicht, einen Werttyp durch Überschreiben von Methoden zu erben. Wenn Werttypen überschreibbare Methoden verwenden, benötigen sie eine virtuelle Methodentabelle, um Aufrufe weiterzuleiten. Dies würde zu den Problemen führen, Strukturen an eine nicht verwaltete Welt weiterzugeben: zusätzliche Felder würden dorthin gehen. Infolgedessen gibt es irgendwo Beschreibungen von Werttypmethoden, auf die Sie jedoch nicht direkt über eine virtuelle Methodentabelle zugreifen können.


Dies könnte die Idee bringen, dass der Mangel an Vererbung künstlich ist


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 .

Dies könnte die Idee bringen, dass der Mangel an Vererbung künstlich ist:


  • es gibt Vererbung von einem Objekt, aber nicht direkt;
  • Innerhalb eines Basistyps befinden sich ToString, Equals und GetHashCode. In Werttypen haben diese Methoden ihr eigenes Verhalten. Dies bedeutet, dass Methoden in Bezug auf ein object überschrieben object .
  • Wenn Sie einen Typ in ein object umwandeln, haben Sie außerdem das volle Recht, ToString, Equals und GetHashCode aufzurufen.
  • Beim Aufrufen einer Instanzmethode für einen Werttyp erhält die Methode eine andere Struktur, die eine Kopie eines Originals ist. Das Aufrufen einer Instanzmethode entspricht dem Aufrufen einer statischen Methode: Method(ref structInstance, newInternalFieldValue) . Dieser Aufruf besteht this mit einer Ausnahme. Eine JIT sollte den Hauptteil einer Methode kompilieren, sodass es nicht erforderlich wäre, Strukturfelder zu versetzen und über den Zeiger auf eine virtuelle Methodentabelle zu springen, die in der Struktur nicht vorhanden ist. Es existiert für Werttypen an einer anderen Stelle .

Das Verhalten der Typen ist unterschiedlich, aber dieser Unterschied ist auf der Ebene der Implementierung in der CLR nicht so groß. Wir werden etwas später darüber sprechen.


Schreiben wir die folgende Zeile in unser Programm:


 var obj = (object)10; 

Es wird uns ermöglichen, mit Nummer 10 unter Verwendung einer Basisklasse umzugehen. Dies nennt man Boxen. Das heißt, wir haben eine VMT, um virtuelle Methoden wie ToString (), Equals und GetHashCode aufzurufen. In Wirklichkeit erstellt das Boxen eine Kopie eines Werttyps, jedoch keinen Zeiger auf ein Original. Dies liegt daran, dass wir den ursprünglichen Wert überall speichern können: auf dem Stapel oder als Feld einer Klasse. Wenn wir es in einen Objekttyp umwandeln, können wir einen Verweis auf diesen Wert so lange speichern, wie wir möchten. Wenn Boxen passiert:


  • Die CLR reserviert Speicherplatz auf dem Heap für eine Struktur + SyncBlockIndex + VMT eines Werttyps (zum Aufrufen von ToString, GetHashCode, Equals).
  • Dort wird eine Instanz eines Werttyps kopiert.

Jetzt haben wir eine Referenzvariante eines Werttyps. Eine Struktur hat absolut die gleichen Systemfelder wie ein Referenztyp .
nach dem Boxen ein vollwertiger Referenztyp werden. Die Struktur wurde eine Klasse. Nennen wir es einen .NET-Salto. Dies ist ein fairer Name.


Schauen Sie sich nur an, was passiert, wenn Sie eine Struktur verwenden, die eine Schnittstelle mit derselben Schnittstelle implementiert.


 struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo(); 

Wenn wir die Foo-Instanz erstellen, geht ihr Wert tatsächlich an den Stapel. Dann setzen wir diese Variable in eine Schnittstellestypvariable und die Struktur in eine Referenztypvariable. Als nächstes gibt es Boxen und wir haben den Objekttyp als Ausgabe. Es handelt sich jedoch um eine Variable vom Typ Schnittstelle. Das heißt, wir brauchen eine Typkonvertierung. Der Anruf erfolgt also folgendermaßen:


 IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo(); 

Das Schreiben eines solchen Codes ist nicht effektiv. Sie müssen eine Kopie anstelle eines Originals ändern:


 void Main() { var foo = new Foo(); foo.a = 1; Console.WriteLite(foo.a); // -> 1 IBoo boo = foo; boo.Boo(); // looks like changing foo.a to 10 Console.WriteLite(foo.a); // -> 1 } struct Foo: IBoo { public int a; public void Boo() { a = 10; } } interface IBoo { void Boo(); } 

Wenn wir uns den Code zum ersten Mal ansehen, müssen wir nicht wissen, womit wir uns im Code befassen, außer in unserem eigenen, und eine Besetzung der IBoo-Oberfläche sehen. Dies lässt uns denken, dass Foo eine Klasse und keine Struktur ist. Dann gibt es keine visuelle Trennung in Strukturen und Klassen, was uns zum Nachdenken bringt
Die Ergebnisse der Schnittstellenmodifikation müssen in foo eingehen, was nicht der Fall ist, da boo eine Kopie von foo ist. Das ist irreführend. Meiner Meinung nach sollte dieser Code Kommentare erhalten, damit andere Entwickler damit umgehen können.


Die zweite Sache bezieht sich auf die vorherigen Gedanken, dass wir einen Typ von einem Objekt in IBoo umwandeln können. Dies ist ein weiterer Beweis dafür, dass ein Boxed-Value-Typ eine Referenzvariante eines Werttyps ist. Oder alle Typen in einem Typsystem sind Referenztypen. Wir können einfach mit Strukturen wie mit Werttypen arbeiten und ihren Wert vollständig übergeben. Dereferenzieren eines Zeigers auf ein Objekt, wie Sie es in der Welt von C ++ sagen würden.


Sie können einwenden, dass wenn es wahr wäre, es so aussehen würde:


 var referenceToInteger = (IInt32)10; 

Wir würden nicht nur ein Objekt erhalten, sondern eine typisierte Referenz für einen Boxed-Value-Typ. Dies würde die gesamte Idee der Werttypen (dh die Integrität ihres Wertes) zerstören und eine großartige Optimierung basierend auf ihren Eigenschaften ermöglichen. Nehmen wir diese Idee auf!


 public sealed class Boxed<T> { public T Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) { return Value.Equals(obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() { return Value.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { return Value.GetHashCode(); } } 

Wir haben ein komplettes Analogon zum Boxen. Wir können den Inhalt jedoch ändern, indem wir Instanzmethoden aufrufen. Diese Änderungen wirken sich auf alle Teile aus, die auf diese Datenstruktur verweisen.


 var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10; 

Die erste Variante ist nicht sehr attraktiv. Anstatt einen Typ zu gießen, schaffen wir Unsinn. Die zweite Zeile ist viel besser, aber die beiden Zeilen sind fast identisch. Der einzige Unterschied besteht darin, dass während des üblichen Boxens nach dem Zuweisen von Speicher auf dem Heap keine Speicherbereinigung mit Nullen erfolgt. Die notwendige Struktur nimmt den Speicher sofort in Anspruch, während die erste Variante gereinigt werden muss. Dadurch arbeitet es um 10% länger als das übliche Boxen.


Stattdessen können wir einige Methoden für unseren Boxwert aufrufen.


 struct Foo { public int x; public void ChangeTo(int newx) { x = newx; } } var boxed = new Boxed<Foo> { Value = new Foo { x = 5 } }; boxed.Value.ChangeTo(10); var unboxed = boxed.Value; 

Wir haben ein neues Instrument. Überlegen wir, was wir damit machen können.


  • Unser Typ Boxed<T> macht dasselbe wie der übliche Typ: Ordnet Speicher auf dem Heap zu, übergibt dort einen Wert und ermöglicht das Abrufen, indem eine Art Unbox ausgeführt wird.
  • Wenn Sie einen Verweis auf eine Boxstruktur verlieren, wird dieser vom GC erfasst.
  • Wir können jetzt jedoch mit einem Box-Typ arbeiten, dh seine Methoden aufrufen.
  • Wir können auch eine Instanz eines Werttyps in SOH / LOH durch eine andere ersetzen. Wir konnten es vorher nicht tun, da wir das Unboxing durchführen, die Struktur in eine andere ändern und zurückboxen müssten, um den Kunden einen neuen Verweis zu geben.

Das Hauptproblem des Boxens ist die Erzeugung von Verkehr im Speicher. Der Verkehr mit unbekannter Anzahl von Objekten, von denen ein Teil bis zur ersten Generation überleben kann, wo wir Probleme mit der Speicherbereinigung bekommen. Es wird viel Müll geben und wir hätten es vermeiden können. Wenn wir jedoch den Verkehr von kurzlebigen Objekten haben, ist die erste Lösung das Pooling. Dies ist ein ideales Ende von .NET Salto.


 var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed); 

Jetzt kann das Boxen mit einem Pool funktionieren, der den Speicherverkehr beim Boxen eliminiert. Wir können sogar Objekte in der Finalisierungsmethode wieder zum Leben erwecken und sich wieder in den Pool zurückversetzen. Dies kann nützlich sein, wenn eine Box-Struktur einen anderen asynchronen Code als Ihren verwendet und Sie nicht verstehen können, wann dies unnötig wurde. In diesem Fall kehrt es während der GC zum Pool zurück.


Lassen Sie uns schließen:


  • Wenn das Boxen versehentlich ist und nicht passieren sollte, lassen Sie es nicht passieren. Dies kann zu Leistungsproblemen führen.
  • Wenn für die Architektur eines Systems Boxen erforderlich ist, kann es Varianten geben. Wenn der Verkehr von Boxstrukturen gering und fast unsichtbar ist, können Sie Boxen verwenden. Wenn der Datenverkehr sichtbar ist, möchten Sie möglicherweise das Boxen mit einer der oben genannten Lösungen zusammenfassen. Es verbraucht einige Ressourcen, sorgt aber dafür, dass GC ohne Überlastung funktioniert.

Schauen wir uns letztendlich einen völlig unpraktischen Code an:


 static unsafe void Main() { // here we create boxed int object boxed = 10; // here we get the address of a pointer to a VMT var address = (void**)EntityPtr.ToPointerWithOffset(boxed); unsafe { // here we get a Virtual Methods Table address var structVmt = typeof(SimpleIntHolder).TypeHandle.Value.ToPointer(); // change the VMT address of the integer passed to Heap into a VMT SimpleIntHolder, turning Int into a structure *address = structVmt; } var structure = (IGetterByInterface)boxed; Console.WriteLine(structure.GetByInterface()); } interface IGetterByInterface { int GetByInterface(); } struct SimpleIntHolder : IGetterByInterface { public int value; int IGetterByInterface.GetByInterface() { return value; } } 

Der Code verwendet eine kleine Funktion, die einen Zeiger von einer Referenz auf ein Objekt erhalten kann. Die Bibliothek ist unter der Github-Adresse verfügbar. Dieses Beispiel zeigt, dass das übliche Boxen int in einen typisierten Referenztyp verwandelt. Lass uns gehen
Schauen Sie sich die Schritte im Prozess an:


  1. Boxen Sie für eine ganze Zahl.
  2. Rufen Sie die Adresse eines erhaltenen Objekts ab (die Adresse von Int32 VMT).
  3. Holen Sie sich die VMT eines SimpleIntHolder
  4. Ersetzen Sie die VMT einer Boxed Integer durch die VMT einer Struktur.
  5. Machen Sie das Unboxing zu einem Strukturtyp
  6. Zeigen Sie den Feldwert auf dem Bildschirm an und erhalten Sie den Int32
    verpackt.

Ich mache es absichtlich über die Schnittstelle, da ich zeigen möchte, dass es funktionieren wird
auf diese Weise.


Nullable \ <T>


Erwähnenswert ist das Verhalten des Boxens mit nullbaren Werttypen. Diese Funktion von Nullable-Werttypen ist sehr attraktiv, da das Boxen eines Werttyps, der eine Art Null ist, Null zurückgibt.


 int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null 

Dies führt uns zu einer eigentümlichen Schlussfolgerung: Da null keinen Typ hat, ist der
Der einzige Weg, um einen Typ zu erhalten, der sich vom Box-Typ unterscheidet, ist der folgende:


 int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed; 

Der Code funktioniert nur, weil Sie einen Typ nach Belieben umwandeln können
mit null.


Tiefer im Boxen


Abschließend möchte ich Ihnen noch etwas über den Typ System.Enum erzählen. Logischerweise sollte dies ein Werttyp sein, da es sich um eine übliche Aufzählung handelt: Aliasing von Zahlen auf Namen in einer Programmiersprache. System.Enum ist jedoch ein Referenztyp. Alle in Ihrem Feld sowie in .NET Framework definierten Enum-Datentypen werden von System.Enum geerbt. Es ist ein Klassendatentyp. Darüber hinaus handelt es sich um eine abstrakte Klasse, die von System.ValueType geerbt wurde.


  [Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... } 

Bedeutet dies, dass alle Aufzählungen auf dem SOH zugeordnet sind und wenn wir sie verwenden, überladen wir den Heap und den GC? Eigentlich nein, da wir sie nur benutzen. Dann nehmen wir an, dass es irgendwo einen Pool von Aufzählungen gibt und wir nur ihre Instanzen erhalten. Nein, schon wieder. Sie können beim Marshalling Aufzählungen in Strukturen verwenden. Aufzählungen sind übliche Zahlen.


Die Wahrheit ist, dass CLR die Datentypstruktur beim Bilden hackt, wenn eine Klasse in einen Werttyp umgewandelt wird :


 // Check to see if the class is a valuetype; but we don't want to mark System.Enum // as a ValueType. To accomplish this, the check takes advantage of the fact // that System.ValueType and System.Enum are loaded one immediately after the // other in that order, and so if the parent MethodTable is System.ValueType and // the System.Enum MethodTable is unset, then we must be building System.Enum and // so we don't mark it as a ValueType. if(HasParent() && ((g_pEnumClass != NULL && GetParentMethodTable() == g_pValueTypeClass) || GetParentMethodTable() == g_pEnumClass)) { bmtProp->fIsValueClass = true; HRESULT hr = GetMDImport()->GetCustomAttributeByName(bmtInternal->pType->GetTypeDefToken(), g_CompilerServicesUnsafeValueTypeAttribute, NULL, NULL); IfFailThrow(hr); if (hr == S_OK) { SetUnsafeValueClass(); } } 

Warum das tun? Insbesondere wegen der Idee der Vererbung - um eine angepasste Aufzählung zu erstellen, müssen Sie beispielsweise die Namen möglicher Werte angeben. Es ist jedoch unmöglich, Werttypen zu erben. Daher haben Entwickler es als Referenztyp entworfen, der es beim Kompilieren in einen Werttyp verwandeln kann.


Was ist, wenn Sie das Boxen persönlich sehen möchten?


Glücklicherweise müssen Sie keinen Disassembler verwenden und in den Code-Dschungel gelangen. Wir haben die Texte des gesamten .NET-Plattformkerns und viele davon sind in Bezug auf .NET Framework CLR und CoreCLR identisch. Sie können auf die unten stehenden Links klicken und die Implementierung des Boxens sofort sehen:



Hier wird die einzige Methode zum Entpacken verwendet:
JIT_Unbox (..) , ein Wrapper um JIT_Unbox_Helper (..) .


Interessant ist auch, dass ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ) Unboxing nicht das Kopieren bedeutet Daten auf den Haufen. Boxen bedeutet, einen Zeiger auf den Heap zu übergeben, während die Kompatibilität der Typen getestet wird. Der IL-Opcode nach dem Entpacken definiert die Aktionen mit dieser Adresse. Die Daten können zum Aufrufen einer Methode in eine lokale Variable oder in den Stapel kopiert werden. Andernfalls hätten wir eine doppelte Kopie; zuerst beim Kopieren vom Heap an einen anderen Ort und dann beim Kopieren an den Zielort.


Fragen


Warum kann .NET CLR kein Pooling für das Boxen selbst durchführen?


Wenn wir mit einem Java-Entwickler sprechen, wissen wir zwei Dinge:


  • Alle Werttypen in Java sind eingerahmt, dh sie sind im Wesentlichen keine Werttypen. Ganzzahlen werden ebenfalls eingerahmt.
  • Aus Optimierungsgründen werden alle Ganzzahlen von -128 bis 127 aus dem Objektpool entnommen.

Warum passiert dies in .NET CLR während des Boxens nicht? Es ist einfach. Da wir den Inhalt eines Boxed-Value-Typs ändern können, können wir Folgendes tun:


 object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138 

Oder so (C ++ / CLI):


 void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; } 

Wenn wir uns mit Pooling befassen würden, würden wir alle in der Anwendung auf 138 ändern, was nicht gut ist.


Das nächste ist die Essenz von Werttypen in .NET. Sie beschäftigen sich mit Wert, was bedeutet, dass sie schneller arbeiten. Boxen ist selten und das Hinzufügen von Boxnummern gehört zur Welt der Fantasie und der schlechten Architektur. Dies ist überhaupt nicht nützlich.


Warum ist es nicht möglich, anstelle des Heaps auf dem Stapel zu boxen, wenn Sie eine Methode aufrufen, die einen Objekttyp annimmt, der tatsächlich ein Werttyp ist?


Wenn das Boxen des Werttyps auf dem Stapel ausgeführt wird und die Referenz auf den Heap verschoben wird, kann die Referenz in der Methode an eine andere Stelle verschoben werden. Beispielsweise kann eine Methode die Referenz in das Feld einer Klasse einfügen. Die Methode wird dann gestoppt und die Methode, die das Boxen gemacht hat, wird ebenfalls gestoppt. Infolgedessen zeigt die Referenz auf einen Totraum auf dem Stapel.


Warum ist es nicht möglich, den Werttyp als Feld zu verwenden?


Manchmal möchten wir eine Struktur als Feld einer anderen Struktur verwenden, die die erste verwendet. Oder einfacher: Verwenden Sie Struktur als Strukturfeld. Fragen Sie mich nicht, warum dies nützlich sein kann. Es kann nicht. Wenn Sie eine Struktur als Feld oder durch Abhängigkeit von einer anderen Struktur verwenden, erstellen Sie eine Rekursion, dh eine Struktur mit unendlicher Größe. In .NET Framework gibt es jedoch einige Stellen, an denen Sie dies tun können. Ein Beispiel ist System.Char , das sich selbst enthält :


 public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... } 

Alle primitiven CLR-Typen sind auf diese Weise entworfen. Wir, bloße Sterbliche, können dieses Verhalten nicht umsetzen. Darüber hinaus brauchen wir das nicht: Es wird getan, um primitiven Typen in CLR einen OOP-Geist zu verleihen.


Dieser Charper wurde von professionellen Übersetzern aus dem Russischen wie aus der Sprache des Autors übersetzt . Sie können uns bei der Erstellung einer übersetzten Version dieses Textes in eine andere Sprache, einschließlich Chinesisch oder Deutsch, unter Verwendung der russischen und englischen Textversion als Quelle helfen.

Wenn Sie "Danke" sagen möchten, können Sie uns am besten einen Stern auf Github oder Forking Repository geben https://github.com/sidristij/dotnetbook

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


All Articles