Warum ist String in C # ein Referenztyp, der sich wie ein Werttyp verhält?


371

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?


Da bei unveränderlichen Typen die Unterscheidung meist ein Implementierungsdetail ist ( isabgesehen 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ächlich isPrüfungen (oder ähnliche Einschränkungen) verwendet.
Elazar

Übrigens ist dies die gleiche Antwort für C ++ (obwohl die Unterscheidung zwischen Wert- und Referenztypen in der Sprache nicht explizit ist), ist die Entscheidung, std::stringsich wie eine Sammlung zu verhalten, ein alter Fehler, der derzeit nicht behoben werden kann.
Elazar

Antworten:


333

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.)


75
Ich wähle hier nicht aus, aber nur, weil ich die Möglichkeit habe, auf einen Blog-Beitrag zu verlinken, der für die Frage relevant ist: Werttypen werden nicht unbedingt auf dem Stapel gespeichert. Dies ist meistens in ms.net der Fall, aber in der CLI-Spezifikation überhaupt nicht angegeben. Der Hauptunterschied zwischen Wert- und Referenztypen besteht darin, dass Referenztypen der Semantik nach Wert kopieren. Siehe blogs.msdn.com/ericlippert/archive/2009/04/27/… und blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn

8
@Qwertie: Stringist keine variable Größe. Wenn Sie es hinzufügen, erstellen Sie tatsächlich ein anderes StringObjekt und weisen ihm neuen Speicher zu.
Codekaizen

5
Das heißt, eine Zeichenfolge könnte theoretisch ein Werttyp (eine Struktur) sein, aber der "Wert" wäre nichts weiter als ein Verweis auf die Zeichenfolge gewesen. Die .NET-Designer haben natürlich beschlossen, den Mittelsmann auszuschalten (die Strukturbehandlung war in .NET 1.0 ineffizient, und es war natürlich, Java zu folgen, in dem Zeichenfolgen bereits als Referenztyp und nicht als primitiver Typ definiert wurden. Plus, wenn Zeichenfolgen vorhanden waren Ein Werttyp, der es dann in ein Objekt konvertiert, würde erfordern, dass es eingerahmt wird (eine unnötige Ineffizienz).
Qwertie

7
@codekaizen Qwertie ist richtig, aber ich denke, der Wortlaut war verwirrend. Eine Zeichenfolge kann eine andere Größe als eine andere Zeichenfolge haben, und daher konnte der Compiler im Gegensatz zu einem echten Werttyp nicht im Voraus wissen, wie viel Speicherplatz zum Speichern des Zeichenfolgenwerts zugewiesen werden muss. Zum Beispiel Int32ist 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 eine intVariable trifft (wenn es sich um einen Werttyp handelt)? Beachten Sie, dass der Wert zu diesem Zeitpunkt noch nicht zugewiesen wurde.
Kevin Brock

2
Entschuldigung, ein Tippfehler in meinem Kommentar, den ich jetzt nicht beheben kann. das hätte sein sollen ... Zum Beispiel Int32ist an immer 4 Bytes, daher weist der Compiler jedes Mal, wenn Sie eine intVariable definieren, 4 Bytes zu . Wie viel Speicher sollte der Compiler zuweisen, wenn er auf eine stringVariable trifft (wenn es sich um einen Werttyp handelt)? Beachten Sie, dass der Wert zu diesem Zeitpunkt noch nicht zugewiesen wurde.
Kevin Brock

57

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

string s = "hello";
string t = "hello";
bool b = (s == t);

eingestellt bsein false? Stellen Sie sich vor, wie schwierig das Codieren für nahezu jede Anwendung wäre.


44
Java ist nicht dafür bekannt, kernig zu sein.
Jason

3
@ Matt: genau. Als ich zu C # wechselte, war das etwas verwirrend, da ich immer (und immer noch manchmal) .equals (..) zum Vergleichen von Strings verwendet habe, während meine Teamkollegen nur "==" verwendeten. Ich habe nie verstanden, warum sie das "==" nicht verlassen haben, um die Referenzen zu vergleichen, obwohl Sie, wenn Sie denken, in 90% der Fälle wahrscheinlich den Inhalt vergleichen möchten, nicht die Referenzen für Zeichenfolgen.
Juri

7
@Juri: Eigentlich denke ich, dass es nie wünschenswert ist, die Referenzen zu überprüfen, da manchmal new String("foo");und andere new String("foo")in derselben Referenz bewerten können, welche Art von nicht das ist, was man von einem newOperator erwarten würde . (Oder können Sie mir einen Fall sagen, in dem ich die Referenzen vergleichen möchte?)
Michael

1
@ Michael Nun, Sie müssen einen Referenzvergleich in alle Vergleiche einbeziehen, um einen Vergleich mit Null zu erhalten. Ein weiterer guter Ort, um Referenzen mit Zeichenfolgen zu vergleichen, ist das Vergleichen und nicht das Vergleichen von Gleichheit. Zwei äquivalente Zeichenfolgen sollten im Vergleich 0 zurückgeben. Das Überprüfen für diesen Fall dauert jedoch so lange, wie der gesamte Vergleich ohnehin durchlaufen wird, ist also keine nützliche Abkürzung. Das Überprüfen auf 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.
Jon Hanna

1
... stringWenn 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, Stringder einen Referenztyp enthält NullableString, wobei der erstere einen Standardwert hat, der dem Standardwert entspricht, String.Emptyund der letztere einen Standardwert hat null, und spezielle Box- / Unboxing-Regeln (wie das Boxen eines Standardtyps). bewertet NullableStringwürde einen Verweis auf String.Empty) ergeben.
Supercat

26

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.


Bei der Unterscheidung zwischen Werttypen und Referenztypen geht es überhaupt nicht um Leistung. Es geht darum, ob eine Variable ein tatsächliches Objekt oder einen Verweis auf ein Objekt enthält. Eine Zeichenfolge kann möglicherweise niemals ein Werttyp sein, da die Größe einer Zeichenfolge variabel ist. es müsste konstant sein, um ein Werttyp zu sein; Leistung hat fast nichts damit zu tun. Die Erstellung von Referenztypen ist auch überhaupt nicht teuer.
Servy

2
@Sevy: Die Größe eines Strings ist konstant.
JacquesB

Weil es nur einen Verweis auf ein Zeichenarray enthält, das eine variable Größe hat. Ein Werttyp zu haben, dessen einziger wirklicher "Wert" ein Referenztyp ist, wäre umso verwirrender, als er für alle intensiven Zwecke immer noch eine Referenzsemantik hätte.
Servy

1
@Sevy: Die Größe eines Arrays ist konstant.
JacquesB

1
Sobald Sie ein Array erstellt haben, ist seine Größe konstant, aber nicht alle Arrays auf der ganzen Welt haben genau die gleiche Größe. Das ist mein Punkt. Damit eine Zeichenfolge ein Wertetyp ist, müssen alle vorhandenen Zeichenfolgen genau dieselbe Größe haben, da auf diese Weise Werttypen in .NET entworfen werden. Es muss in der Lage sein, Speicherplatz für solche Werttypen zu reservieren, bevor tatsächlich ein Wert vorhanden ist. Daher muss die Größe zum Zeitpunkt der Kompilierung bekannt sein . Ein solcher stringTyp müsste einen Zeichenpuffer mit einer festen Größe haben, der sowohl restriktiv als auch äußerst ineffizient wäre.
Servy

16

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.

Stringist ein Referenztyp anstelle eines Werttyps, da es für Microsoft von entscheidender Bedeutung war, sicherzustellen, dass Zeichenfolgen in nicht generischen Sammlungen wie z System.Collections.ArrayList.

Das Speichern eines Werttyps in einer nicht generischen Sammlung erfordert eine spezielle Konvertierung in den Typ, objectder als Boxen bezeichnet wird. Wenn die CLR einen Werttyp einfügt, wird der Wert in a eingeschlossen System.Objectund 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 stringsie 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.


Mein Dank für diese Antwort! Ich habe mir alle anderen Antworten angesehen, die Dinge über Heap- und Stack-Zuordnungen sagen, während Stack ein Implementierungsdetail ist . Immerhin stringenthält nur seine Größe und einen Zeiger auf das charArray trotzdem, so wäre es nicht ein „großer Werttyp“ sein. Dies ist jedoch ein einfacher, relevanter Grund für diese Entwurfsentscheidung. Vielen Dank!
V0ldek

8

Nicht nur Zeichenfolgen sind unveränderliche Referenztypen. Auch Multi-Cast-Delegierte. Deshalb ist es sicher zu schreiben

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

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

string s1 = "my string";
//some code here
string s2 = "my string";

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.


1
Es gibt viele Beispiele für unveränderliche Referenztypen. Und was das String-Beispiel
betrifft,

5
Re letzten Punkt: String nicht hilft , wenn Sie versuchen , passieren eine große Zeichenfolge (da es tatsächlich als String ohnehin implementiert) - Stringbuilder für nützlich ist die Manipulation eine Zeichenfolge mehrmals.
Marc Gravell

Meinten Sie Delegiertenhandler, nicht Hadler? (Entschuldigung, wählerisch zu sein .. aber es ist sehr nah an einem (nicht gebräuchlichen) Nachnamen, den ich kenne ....)
Pure.Krome

6

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?


5

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.


3

In sehr einfachen Worten kann jeder Wert, der eine bestimmte Größe hat, als Werttyp behandelt werden.


Dies sollte ein Kommentar sein
ρяσssρєя K

leichter zu verstehen für ppl neu in c #
LONG

2

Wie können Sie feststellen, ob stringes 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.


Es ist ein Referenztyp (glaube ich), da er nicht von System.ValueType von MSDN abgeleitet ist. Anmerkungen zu System.ValueType: Datentypen werden in Werttypen und Referenztypen unterteilt. Werttypen werden entweder stapelweise zugewiesen oder in einer Struktur inline zugewiesen. Referenztypen werden Heap-zugeordnet.
Davy8

Sowohl Referenz- als auch Werttypen werden von der ultimativen Basisklasse Object abgeleitet. In Fällen, in denen sich ein Werttyp wie ein Objekt verhalten muss, wird dem Heap ein Wrapper zugewiesen, der den Werttyp wie ein Referenzobjekt aussehen lässt, und der Wert des Werttyps wird in diesen kopiert.
Davy8

Der Wrapper ist markiert, damit das System weiß, dass er einen Werttyp enthält. Dieser Vorgang wird als Boxen bezeichnet, und der umgekehrte Vorgang wird als Unboxing bezeichnet. Beim Ein- und Auspacken kann jeder Typ als Objekt behandelt werden. (Im
Hintergrund

2

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.


Mir ist klar, dass Werttypen per Definition nicht unveränderlich sind, aber die meisten Best Practices scheinen darauf hinzudeuten, dass dies beim Erstellen eigener Werte der Fall sein sollte. Ich sagte Eigenschaften, nicht Eigenschaften von
Werttypen

5
@WebMatrix, @ Davy8: Die primitiven Typen (int, double, bool, ...) sind unveränderlich.
Jason

1
@Jason, ich dachte, ein unveränderlicher Begriff gilt hauptsächlich für Objekte (Referenztypen), die sich nach der Initialisierung nicht ändern können, wie z. B. Zeichenfolgen, wenn sich der Wert der Zeichenfolgen ändert, intern eine neue Instanz einer Zeichenfolge erstellt wird und das ursprüngliche Objekt unverändert bleibt. Wie trifft dies auf Werttypen zu?
WebMatrix

8
Irgendwie ist es in "int n = 4; n = 9;" nicht so, dass Ihre int-Variable "unveränderlich" im Sinne von "konstant" ist; Es ist so, dass der Wert 4 unveränderlich ist und sich nicht in 9 ändert. Ihre int-Variable "n" hat zuerst den Wert 4 und dann einen anderen Wert, 9; aber die Werte selbst sind unveränderlich. Ehrlich gesagt ist mir das sehr nahe an wtf.
Daniel Daranas

1
+1. Ich habe es satt zu hören, dass "Strings wie Werttypen sind", wenn sie es ganz einfach nicht sind.
Jon Hanna

1

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).


1
"Zeichenfolgengröße ist nicht bekannt, bevor sie zugewiesen wird" - dies ist in der CLR falsch.
Codekaizen

-1

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.