Ein String ist ein Referenztyp, obwohl er die meisten Merkmale eines Werttyps aufweist, z. B. unveränderlich und == überladen, um den Text zu vergleichen, anstatt sicherzustellen, dass sie auf dasselbe Objekt verweisen.
Warum ist String dann nicht nur ein Werttyp?
c#
string
clr
value-type
reference-type
Davy8
quelle
quelle
is
abgesehen von Tests), lautet die Antwort wahrscheinlich "aus historischen Gründen". Die Kopierleistung kann nicht der Grund sein, da unveränderliche Objekte nicht physisch kopiert werden müssen. Jetzt ist es unmöglich, Änderungen vorzunehmen, ohne den Code zu beschädigen, der tatsächlichis
Prüfungen (oder ähnliche Einschränkungen) verwendet.std::string
sich wie eine Sammlung zu verhalten, ein alter Fehler, der derzeit nicht behoben werden kann.Antworten:
Zeichenfolgen sind keine Werttypen, da sie sehr groß sein können und auf dem Heap gespeichert werden müssen. Werttypen werden (in allen Implementierungen der CLR bis jetzt) auf dem Stapel gespeichert. Das Zuweisen von Zeichenfolgen durch den Stapel würde alle möglichen Probleme lösen: Der Stapel ist nur 1 MB für 32-Bit und 4 MB für 64-Bit. Sie müssten jede Zeichenfolge boxen, was zu einer Kopierstrafe führen würde, Sie könnten keine Zeichenfolgen internieren und den Speicher belegen würde Ballon, etc ...
(Bearbeiten: Es wurde eine Klarstellung hinzugefügt, dass die Speicherung von Werttypen ein Implementierungsdetail ist, was zu dieser Situation führt, in der wir einen Typ mit Wertesematiken haben, die nicht von System.ValueType erben. Danke Ben.)
quelle
String
ist keine variable Größe. Wenn Sie es hinzufügen, erstellen Sie tatsächlich ein anderesString
Objekt und weisen ihm neuen Speicher zu.Int32
ist an immer 4 Bytes, daher weist der Compiler jedes Mal, wenn Sie eine Zeichenfolgenvariable definieren, 4 Bytes zu. Wie viel Speicher sollte der Compiler zuweisen, wenn er auf eineint
Variable trifft (wenn es sich um einen Werttyp handelt)? Beachten Sie, dass der Wert zu diesem Zeitpunkt noch nicht zugewiesen wurde.Int32
ist an immer 4 Bytes, daher weist der Compiler jedes Mal, wenn Sie eineint
Variable definieren, 4 Bytes zu . Wie viel Speicher sollte der Compiler zuweisen, wenn er auf einestring
Variable trifft (wenn es sich um einen Werttyp handelt)? Beachten Sie, dass der Wert zu diesem Zeitpunkt noch nicht zugewiesen wurde.Es ist kein Werttyp, da die Leistung (Raum und Zeit!) Schrecklich wäre, wenn es sich um einen Werttyp handeln würde und sein Wert jedes Mal kopiert werden müsste, wenn er an Methoden usw. übergeben und von diesen zurückgegeben wird.
Es hat Wertesemantik, um die Welt gesund zu halten. Können Sie sich vorstellen, wie schwierig es wäre, zu codieren, wenn
eingestellt
b
seinfalse
? Stellen Sie sich vor, wie schwierig das Codieren für nahezu jede Anwendung wäre.quelle
new String("foo");
und anderenew String("foo")
in derselben Referenz bewerten können, welche Art von nicht das ist, was man von einemnew
Operator erwarten würde . (Oder können Sie mir einen Fall sagen, in dem ich die Referenzen vergleichen möchte?)ReferenceEquals(x, y)
ist ein schneller Test, und Sie können sofort 0 zurückgeben. Wenn Sie ihn mit Ihrem Null-Test mischen, wird nicht einmal mehr Arbeit hinzugefügt.string
Wenn Zeichenfolgen eher ein Werttyp dieses Stils als ein Klassentyp sind, bedeutet dies, dass sich der Standardwert von a eher als leere Zeichenfolge (wie in pre.net-Systemen) als als Nullreferenz verhalten könnte. Eigentlich würde ich es vorziehen, einen Werttyp zu haben,String
der einen Referenztyp enthältNullableString
, wobei der erstere einen Standardwert hat, der dem Standardwert entspricht,String.Empty
und der letztere einen Standardwert hatnull
, und spezielle Box- / Unboxing-Regeln (wie das Boxen eines Standardtyps). bewertetNullableString
würde einen Verweis aufString.Empty
) ergeben.Die Unterscheidung zwischen Referenztypen und Werttypen ist grundsätzlich ein Leistungskompromiss bei der Gestaltung der Sprache. Referenztypen haben einen gewissen Aufwand für Konstruktion und Zerstörung sowie für die Speicherbereinigung, da sie auf dem Heap erstellt werden. Werttypen hingegen haben Overhead bei Methodenaufrufen (wenn die Datengröße größer als ein Zeiger ist), da das gesamte Objekt kopiert wird und nicht nur ein Zeiger. Da Zeichenfolgen viel größer als die Größe eines Zeigers sein können (und normalerweise sind), werden sie als Referenztypen entworfen. Wie Servy hervorhob, muss die Größe eines Wertetyps zur Kompilierungszeit bekannt sein, was bei Zeichenfolgen nicht immer der Fall ist.
Die Frage der Veränderlichkeit ist ein gesondertes Thema. Sowohl Referenztypen als auch Werttypen können entweder veränderlich oder unveränderlich sein. Werttypen sind jedoch normalerweise unveränderlich, da die Semantik für veränderbare Werttypen verwirrend sein kann.
Referenztypen sind im Allgemeinen veränderlich, können jedoch als unveränderlich konzipiert werden, wenn dies sinnvoll ist. Zeichenfolgen werden als unveränderlich definiert, da sie bestimmte Optimierungen ermöglichen. Wenn beispielsweise dasselbe Zeichenfolgenliteral im selben Programm mehrmals vorkommt (was durchaus üblich ist), kann der Compiler dasselbe Objekt wiederverwenden.
Warum ist "==" überladen, um Zeichenfolgen nach Text zu vergleichen? Weil es die nützlichste Semantik ist. Wenn zwei Zeichenfolgen im Text gleich sind, können sie aufgrund der Optimierungen dieselbe Objektreferenz sein oder nicht. Das Vergleichen von Referenzen ist also ziemlich nutzlos, während das Vergleichen von Text fast immer das ist, was Sie wollen.
Sprechen allgemein hat Strings , was bezeichnet wird Wert Semantik . Dies ist ein allgemeineres Konzept als Werttypen, bei denen es sich um ein C # -spezifisches Implementierungsdetail handelt. Werttypen haben eine Wertesemantik, aber Referenztypen können auch eine Wertesemantik haben. Wenn ein Typ eine Wertsemantik aufweist, können Sie nicht wirklich feststellen, ob es sich bei der zugrunde liegenden Implementierung um einen Referenztyp oder einen Werttyp handelt. Sie können dies also als Implementierungsdetail betrachten.
quelle
string
Typ müsste einen Zeichenpuffer mit einer festen Größe haben, der sowohl restriktiv als auch äußerst ineffizient wäre.Dies ist eine späte Antwort auf eine alte Frage, aber allen anderen Antworten fehlt der Punkt, nämlich, dass .NET bis .NET 2.0 im Jahr 2005 keine Generika hatte.
String
ist ein Referenztyp anstelle eines Werttyps, da es für Microsoft von entscheidender Bedeutung war, sicherzustellen, dass Zeichenfolgen in nicht generischen Sammlungen wie zSystem.Collections.ArrayList
.Das Speichern eines Werttyps in einer nicht generischen Sammlung erfordert eine spezielle Konvertierung in den Typ,
object
der als Boxen bezeichnet wird. Wenn die CLR einen Werttyp einfügt, wird der Wert in a eingeschlossenSystem.Object
und auf dem verwalteten Heap gespeichert.Das Lesen des Werts aus der Sammlung erfordert die inverse Operation, die als Unboxing bezeichnet wird.
Sowohl das Boxen als auch das Unboxing verursachen nicht zu vernachlässigende Kosten: Das Boxen erfordert eine zusätzliche Zuordnung, das Unboxing erfordert eine Typprüfung.
Einige Antworten behaupten fälschlicherweise, dass
string
sie niemals als Werttyp implementiert werden könnten, da ihre Größe variabel ist. Tatsächlich ist es einfach, eine Zeichenfolge als Datenstruktur mit fester Länge mithilfe einer Strategie zur Optimierung kleiner Zeichenfolgen zu implementieren: Zeichenfolgen werden direkt als Folge von Unicode-Zeichen im Speicher gespeichert, mit Ausnahme großer Zeichenfolgen, die als Zeiger auf einen externen Puffer gespeichert werden. Beide Darstellungen können so gestaltet werden, dass sie dieselbe feste Länge haben, dh die Größe eines Zeigers.Wenn Generika vom ersten Tag an existiert hätten, wäre es wahrscheinlich eine bessere Lösung gewesen, einen String als Wertetyp zu haben, mit einer einfacheren Semantik, einer besseren Speichernutzung und einer besseren Cache-Lokalität. Eine,
List<string>
die nur kleine Zeichenfolgen enthält, könnte ein einzelner zusammenhängender Speicherblock gewesen sein.quelle
string
enthält nur seine Größe und einen Zeiger auf daschar
Array trotzdem, so wäre es nicht ein „großer Werttyp“ sein. Dies ist jedoch ein einfacher, relevanter Grund für diese Entwurfsentscheidung. Vielen Dank!Nicht nur Zeichenfolgen sind unveränderliche Referenztypen. Auch Multi-Cast-Delegierte. Deshalb ist es sicher zu schreiben
Ich nehme an, dass Zeichenfolgen unveränderlich sind, da dies die sicherste Methode ist, um mit ihnen zu arbeiten und Speicher zuzuweisen. Warum sind sie keine Werttypen? Frühere Autoren haben Recht mit der Stapelgröße usw. Ich möchte auch hinzufügen, dass das Festlegen von Zeichenfolgen als Referenztypen das Einsparen von Baugruppengröße ermöglicht, wenn Sie dieselbe konstante Zeichenfolge im Programm verwenden. Wenn Sie definieren
Es besteht die Möglichkeit, dass beide Instanzen der Konstante "my string" in Ihrer Assembly nur einmal zugewiesen werden.
Wenn Sie Zeichenfolgen wie gewohnt als Referenztyp verwalten möchten, fügen Sie die Zeichenfolge in einen neuen StringBuilder (Zeichenfolge s) ein. Oder verwenden Sie MemoryStreams.
Wenn Sie eine Bibliothek erstellen möchten, in der Sie erwarten, dass in Ihren Funktionen große Zeichenfolgen übergeben werden, definieren Sie einen Parameter entweder als StringBuilder oder als Stream.
quelle
Außerdem die Art und Weise, wie Zeichenfolgen implementiert werden (für jede Plattform unterschiedlich) und wann Sie sie zusammenfügen. Wie mit einem
StringBuilder
. Es weist Ihnen einen Puffer zu, in den Sie kopieren können, sobald Sie das Ende erreicht haben. Es weist Ihnen noch mehr Speicher zu, in der Hoffnung, dass eine große Verkettungsleistung nicht beeinträchtigt wird, wenn Sie dies tun.Vielleicht kann Jon Skeet hier oben helfen?
quelle
Es ist hauptsächlich ein Leistungsproblem.
Wenn sich Zeichenfolgen wie der Werttyp verhalten, hilft dies beim Schreiben von Code, aber wenn es sich um einen Werttyp handelt, würde dies einen enormen Leistungseinbruch bedeuten.
Werfen Sie einen Blick auf einen schönen Artikel über Zeichenfolgen im .net-Framework , um einen detaillierten Überblick zu erhalten.
quelle
In sehr einfachen Worten kann jeder Wert, der eine bestimmte Größe hat, als Werttyp behandelt werden.
quelle
Wie können Sie feststellen, ob
string
es sich um einen Referenztyp handelt? Ich bin mir nicht sicher, ob es darauf ankommt, wie es implementiert wird. Zeichenfolgen in C # sind genau unveränderlich, damit Sie sich über dieses Problem keine Sorgen machen müssen.quelle
Tatsächlich haben Zeichenfolgen nur sehr wenige Ähnlichkeiten mit Werttypen. Für den Anfang sind nicht alle Werttypen unveränderlich. Sie können den Wert eines Int32 beliebig ändern, und es wäre immer noch dieselbe Adresse auf dem Stapel.
Strings sind aus einem sehr guten Grund unveränderlich. Sie haben nichts damit zu tun, dass es sich um einen Referenztyp handelt, sondern viel mit der Speicherverwaltung. Es ist nur effizienter, ein neues Objekt zu erstellen, wenn sich die Zeichenfolgengröße ändert, als Dinge auf dem verwalteten Heap zu verschieben. Ich denke, Sie mischen Wert- / Referenztypen und unveränderliche Objektkonzepte miteinander.
Soweit "==": Wie Sie sagten, ist "==" eine Operatorüberladung, und sie wurde erneut aus einem sehr guten Grund implementiert, um das Framework bei der Arbeit mit Zeichenfolgen nützlicher zu machen.
quelle
Ist nicht so einfach, wie Strings aus Zeichenarrays bestehen. Ich betrachte Strings als Zeichenarrays []. Daher befinden sie sich auf dem Heap, da der Referenzspeicherort auf dem Stapel gespeichert ist und auf den Anfang des Speicherorts des Arrays auf dem Heap zeigt. Die Zeichenfolgengröße ist nicht bekannt, bevor sie zugewiesen wird ... perfekt für den Heap.
Aus diesem Grund ist eine Zeichenfolge wirklich unveränderlich, da der Compiler dies nicht weiß, wenn Sie sie ändern, auch wenn sie dieselbe Größe hat, und ein neues Array zuweisen und den Positionen im Array Zeichen zuweisen muss. Es ist sinnvoll, wenn Sie Strings als eine Möglichkeit betrachten, mit der Sprachen Sie davor schützen, Speicher im laufenden Betrieb zuweisen zu müssen (lesen Sie C wie Programmierung).
quelle
Es besteht die Gefahr, dass ein weiterer mysteriöser Abstimmungspunkt erreicht wird. Viele erwähnen den Stapel und den Speicher in Bezug auf Werttypen und primitive Typen, weil sie in ein Register im Mikroprozessor passen müssen. Sie können nichts zum / vom Stapel verschieben oder ablegen, wenn es mehr Bits benötigt als ein Register. Die Anweisungen lauten beispielsweise "pop eax", da eax auf einem 32-Bit-System 32 Bit breit ist.
Gleitkomma-Primitivtypen werden von der 80 Bit breiten FPU verarbeitet.
Dies alles wurde lange bevor es eine OOP-Sprache gab, um die Definition des primitiven Typs zu verschleiern, entschieden, und ich gehe davon aus, dass der Werttyp ein Begriff ist, der speziell für OOP-Sprachen erstellt wurde.
quelle