Generics Speicherverwaltung

8

Ich habe eine Frage, wie Speicher für starke Generics

verwaltet wird %Vor%

Frage 1: Ich nehme an, da ints1 mit einem neuen Schlüsselwort ( new List<int>() ) initialisiert wird, wird es ein Referenztyp. Wo sind die Werte 1,2,3 im Speicher abgelegt (sind sie im Stack oder auf Heap gespeichert)?

Frage 2: Ich weiß etwas zwischen List<int> und int[] , List<int> kann seine Größe zur Laufzeit auf eine beliebige Größe skalieren, die zur Kompilierzeit in int[] eingeschränkt ist. Also, wenn die Werte 1,2,3 und im Stapel gespeichert sind, wenn ein neues Element zu List<int> say 4 hinzugefügt wird, wäre es nicht kontinuierlich zu den ersten 3 rechts, also wie weiß ints1 die Speicherstelle von 4 ?

    
Gururaj 30.12.2011, 11:05
quelle

7 Antworten

14
  

Ich nehme an, da ints1 mit einem neuen Schlüsselwort new List<int>() initialisiert wird, wird es ein Referenztyp

Diese Annahme ist falsch. Sie können das Schlüsselwort "new" auch für einen Werttyp verwenden!

%Vor%

Die Verwendung von "new" macht nichts zu einem Referenztyp. Sie können "neu" mit Referenztypen oder Werttypen verwenden. Was "neu" anzeigt, ist, dass Speicher zugewiesen wird und ein Konstruktor wird heißen.

Wenn "new" für einen Werttyp verwendet wird, ist der zugewiesene Speicher ein temporärer Speicher. Ein Verweis auf diesen temporären Speicher wird an den Konstruktor übergeben. Anschließend wird das jetzt initialisierte Ergebnis an das endgültige Ziel kopiert, sofern vorhanden. ("Neu" wird normalerweise mit einer Zuweisung verwendet, muss aber nicht sein.)

Bei einem Referenztyp wird der Speicher doppelt zugeordnet: Der Langzeitspeicher wird für die Instanz und der Kurzzeitspeicher für den Verweis auf den Langzeitspeicher reserviert der Instanz Die Referenz wird an den Konstruktor übergeben, der den Langzeitspeicher initialisiert. Die Referenz wird dann vom Kurzzeitspeicher zum endgültigen Ziel kopiert, sofern vorhanden.

Was List<int> zu einem Referenztyp macht, ist, dass List<T> als Klasse deklariert ist.

  

Wo sind die Werte 1,2,3 im Speicher abgelegt (sind sie im Stack oder auf Heap gespeichert)?

Wir haben hart daran gearbeitet, einen Speichermanager zu erstellen, der es Ihnen egal macht, wo Dinge gespeichert sind. Werte werden entweder in einem Kurzzeitspeicherpool (der als Stapel oder Register implementiert ist) oder in einem Langzeitspeicherpool (der als Sammelspeicher für überflüssige Speicher implementiert ist) gespeichert. Speicher wird abhängig von der bekannten Lebensdauer des Werts zugewiesen. Wenn bekannt ist, dass der Wert kurzlebig ist, wird sein Speicher im Kurzzeitpool zugeordnet. Wenn der Wert nicht als kurzlebig bekannt ist, muss er dem Langzeitpool zugeordnet werden.

Die 1, 2, 3, die der Liste gehören, könnten für immer leben; wir wissen nicht, ob diese Liste den aktuellen Aktivierungsrahmen überlebt oder nicht. Daher ist der Speicher zum Speichern der 1, 2, 3 auf dem Langzeitpool reserviert.

Glauben Sie nicht der Lüge, dass "Werttypen immer auf dem Stapel zugeordnet sind". Offensichtlich kann das nicht wahr sein, weil dann eine Klasse oder ein Array, die eine Zahl enthalten, den aktuellen Stapelrahmen nicht überleben könnte! Werttypen werden dem Pool zugeordnet, der für ihre bekannte Lebensdauer sinnvoll ist.

  

List<int> kann seine Größe zur Laufzeit auf eine beliebige Größe skalieren, im Gegensatz zu int[]

Richtig. Es ist lehrreich zu sehen, wie List<T> das macht. Es teilt einfach ein Array von T größer als es benötigt. Wenn es feststellt, dass es zu klein geraten ist, weist es ein neues, größeres Array zu und kopiert den alten Array-Inhalt in den neuen. A List<T> ist nur ein praktischer Wrapper um eine Reihe von Array-Kopien!

  

Wenn die Werte 1,2,3 im Stack gespeichert wurden und ein neues Element 4 zur Liste hinzugefügt wird, dann wäre es nicht kontinuierlich mit den ersten drei.

Richtig. Das ist einer der Gründe, warum der Speicher für die Werte 1, 2, 3 nicht auf dem Stack zugeordnet ist. Der Speicher ist eigentlich ein Array, das auf dem Heap zugeordnet ist.

  

Wie wird die Liste den Speicherort von Element 4 wissen?

Die Liste weist ein Array zu, das zu groß ist. Wenn Sie ein neues Objekt hinzufügen, wird es in den nicht benötigten Speicherplatz im zu großen Array eingefügt. Wenn das Array keinen Platz mehr hat, weist es ein neues Array zu.

    
Eric Lippert 30.12.2011 16:50
quelle
11

Die "neue" Syntax wird verwendet, um sowohl Werttypen als auch Referenztypen zu initialisieren. Die neue Liste wird auf dem Heap erstellt. Die Werte werden auf dem Stapel geladen (d. h. bevor sie der Liste hinzugefügt werden), aber sobald sie hinzugefügt werden, befinden sie sich auf dem Heap in der int[] , die die Liste untermauert. Arrays sind immer auf dem Heap.

Die Tatsache, dass sie in das Array kopiert werden, beantwortet auch Teil 2, glaube ich. Das Array ist übergroß und wird nur dann neu zugewiesen, wenn es voll ist.

Hinweis; List<int> wird nicht zum Referenztyp; Es ist immer ein Referenztyp.

    
Marc Gravell 30.12.2011 11:13
quelle
6

Die Speicherverwaltung für Generika (generische Sammlungen) entspricht genau der für nicht generische Typen.

Ihre ints1 Liste verwendet ein Array unter den Deckblättern. Es ist dasselbe wie für ints2 (wenn es korrigiert wurde). In beiden Fällen enthält ein Speicherblock auf dem Heap die int -Werte.

Die List<> Klasse besteht aus einem Array, einer int Count und einer int Capacity Eigenschaft. Wenn Sie Add () hinzufügen, wird ein Element Count inkrementiert, wenn es Capacity übergibt, wird ein neues Array zugewiesen und der Inhalt wird kopiert.

    
Henk Holterman 30.12.2011 11:12
quelle
2

Frage 1: Ссылка sagt:

  

Die List-Klasse ist das generische Äquivalent der ArrayList-Klasse. Es   implementiert die generische IList-Schnittstelle unter Verwendung eines Arrays mit der Größe   dynamisch erhöht wie erforderlich.

Dies sieht wie ein einfaches Array aus, das nur neu zugewiesen wird, wenn es überläuft. AFAIKR die Größe verdoppelt sich bei jeder Neuzuweisung - ich habe das einmal recherchiert, kann mich aber nicht mehr an was erinnern.

Das Array wird auf dem verwalteten Heap zugeordnet, genau wie wenn Sie es gerade deklariert hätten.

    
Eugen Rieck 30.12.2011 11:16
quelle
2
  1. Liste ist ein Referenztyp, egal wie Sie ihn sehen. Alle diese Typen sind auf dem Heap zugeordnet. Ich weiß nicht, ob der C # -Compiler noch schlau genug ist, um herauszufinden, dass ein Objekt, das nicht außerhalb einer Methode verwendet wird, auf dem Stack zugewiesen werden kann (Eric Lippert könnte es uns sagen), aber selbst wenn, Das ist etwas, um das Sie sich als Programmierer nicht kümmern müssen. Es wird nur eine Optimierung sein, die der Compiler für Sie tun wird, ohne dass Sie es jemals merken.

  2. Ein Array von int ist auch ein Referenztyp und es ist auch auf dem Heap zugeordnet, genauso einfach ist es. Es macht keinen Sinn, sich über eine hypothetische Fragmentierung von Arrays im Stack Gedanken zu machen, da sie einfach nicht im Stack zugeordnet sind.

Mike Nakis 30.12.2011 11:19
quelle
1
%Vor%      

Ich gehe davon aus, dass ints1 mit einem neuen Schlüsselwort (new List ()) initialisiert wird und zu einem Referenztyp wird.

Lassen Sie uns zuerst einige Begriffe klären:

  • ints1 ist kein -Typ irgendeiner Art, sondern eine -Variable , so dass es niemals "ein Referenztyp werden kann".
  • Variablen haben einen statischen Typ ("Kompilierzeit"). ints1 wurde zum Beispiel vom Typ List<int> deklariert. Der statische Typ einer Variablen ändert sich nie.
  • Der Typ des Werts oder Objekts, auf den eine Variable verweist, wird dynamischer Typ ("Laufzeit") genannt. Nach der obigen Zuweisung ist der dynamische Typ von ints1 List<int> . Der dynamische Typ einer Variablen kann sich ändern, wenn ihr ein neuer Wert oder ein neues Objekt zugewiesen wird.

Im Falle von ints1 sind beide Typen gleich, aber das ist nicht immer so:

%Vor%

Hier ist der statische Typ ICollection<int> (immer) und der dynamische Typ List<int> (nach der Zuweisung).

Beantworten Sie die Frage 1:

%Vor%      

Wo sind die Werte 1,2,3 im Speicher gespeichert (sind sie im Stack oder auf dem Heap gespeichert)?

Die offizielle Antwort lautet, dass dies als Implementierungsdetail von List<T> betrachtet werden sollte und für Sie keine Rolle spielen sollte. Alles, was Sie wissen müssen, sind die Vorgänge, die Sie für List<T> ausführen können (z. B. Add , Clear usw.) und deren Merkmale (wie Vor- / Nachbedingungen, Leistung usw.).

Wenn Sie immer noch wissen möchten, wie dieser Typ intern funktioniert, beginnen wir mit diesem Hinweis:

  

Die Klasse List<T> implementiert die generische Schnittstelle IList<T> mit einem Array, dessen Größe je nach Bedarf dynamisch erhöht wird. - MSDN-Referenzseite für List<T> , Abschnitt Anmerkungen

Das heißt, Sie können sich ein List<T> als ein Array vom Typ T[] vorstellen, das bei Bedarf seine Kapazität erhöht. Im Fall von ints1 , denke an einen int[] . Da int ein Werttyp ist, werden die konkreten Werte in der Liste (1, 2, 3) direkt gespeichert. Wenn es ein Referenztyp wäre, würden die Werte jeweils an einem separaten Ort gespeichert und das Array würde nur Referenzen enthalten.

Beachten Sie, dass ich die Begriffe "Stack" und "Heap" im obigen Absatz nicht erwähnt habe. Dies liegt daran, dass diese Konzepte ein weiteres Implementierungsdetail sind, nämlich eine der .NET-Plattform, über die Sie sich nicht zu sehr kümmern sollten. (Siehe Eric Lipperts Blogartikelserie, "Der Stack ist ein Implementierungsdetail" , und z. B. meine Antwort auf eine vorherige Frage: "Weist new immer auf dem Heap auf in C ++ / C # / Java? ")

Antwort auf Frage 2:

  

List kann seine Größe zur Laufzeit auf eine beliebige Größe skalieren, die zur Kompilierungszeit in int [] eingeschränkt ist. Also, wenn die Werte 1,2,3 und im Stapel gespeichert sind, wenn ein neues Element zu Liste z. B. 4 hinzugefügt wird, wäre es nicht kontinuierlich zu den ersten 3 rechts, also wie weiß ints1 den Speicherort von 4?

Auch hier musst du nicht darüber nachdenken. Was an List<int> von Bedeutung ist, ist, ob seine besonderen Merkmale und Operationen, die es bietet, Ihren Zwecken entsprechen. Aber ich hätte deine Frage nicht wirklich beantwortet, wenn ich es dabei belassen würde, also lass uns kurz darüber nachdenken:

Wenn Sie über den "Stack" sprechen, meinen Sie wahrscheinlich den Call-Stack eines ausführenden Threads . AFAIK, virtuelle Maschinen und Programmiersprachen, die eine solche Datenstruktur verwenden, verwenden sie meistens, um Argumente an Funktionen / Methoden zu übergeben und vielleicht auch um Werte zu speichern, die für eine Funktion lokal bleiben. Dies ist nicht gleichbedeutend damit, dass der Aufrufstack auch für lokale Variablen verwendet wird, da eine lokale Variable ein Objekt enthalten kann, das entweder über den Rückgabewert oder über ein ref / out an die aufrufende Funktion zurückgegeben wird. Parameter usw.

Mir scheint es unwahrscheinlich, dass ein List<int> -Objekt jemals unter normalen Umständen auf einem Call-Stack landen würde (ich denke nicht an stackalloc oder irgendetwas anderes im Zusammenhang mit unsafe Kontexte gleichzeitig.). Denken Sie daran, dass List<T> ein Referenztyp ist: Zwar ist es möglich, und möglicherweise ziemlich wahrscheinlich, dass der Verweis auf das tatsächliche Objekt auf dem Aufrufstapel landet, höchstwahrscheinlich werden die Daten des Objekts selbst nicht.

Eine (vielleicht naive) Implementierung von IList<T> , die Arrays fester Größe für die Speicherung von Elementen verwendet und angesichts der Notwendigkeit, ihre Kapazität zu erhöhen, könnte dynamisch ein neues, größeres Array zuweisen und dann den Inhalt des aktuellen Arrays kopieren zu dem neuen Array und dann das alte Array zugunsten des neuen Array.Auf diese Weise bleiben alle Elemente an einem Ort zusammen.

    
stakx 30.12.2011 12:27
quelle
0

Frage 1

Listen in C # enthalten intern Arrays. Liste bezieht sich auf einen Speicherort auf dem Heap, und an diesem Speicherort befindet sich ein Array, in dem alle Werte gespeichert sind. Die Werte werden also auf dem Heap gespeichert. Dasselbe gilt für Arrays, wenn es Teil einer Klasse ist.

Frage 2

Der Stapel ist fortlaufend. Wenn Sie also ein anderes int auf dem Stapel drücken, bedeutet dies, dass die Speicheradresse dem vorherigen int + 4 entspricht. Die Funktionsweise von Listen beim Hinzufügen von Elementen besteht darin, dass sie ein Array erstellen, das größer ist als das, was Sie benötigen . Wenn Sie die Länge des Arrays erreicht haben, gibt es einen Algorithmus, der ein größeres Array erstellt und die aktuellen Werte kopiert.

Eine andere Sache, die Sie interessieren könnte, sind verknüpfte Listen. Verknüpfte Listen funktionieren nicht intern mit Arrays, sondern mit Knoten. Jeder Knoten enthält Daten und die Position des nächsten Knotens in der Liste. Eine doppelt verknüpfte Liste enthält Knoten mit all dem und den Ort des vorherigen Knotens in der Liste.

    
Robert Rouhani 30.12.2011 11:17
quelle

Tags und Links