En C #, pourquoi String est-il un type de référence qui se comporte comme un type de valeur?

371

Une chaîne est un type de référence même si elle possède la plupart des caractéristiques d'un type de valeur, comme être immuable et avoir == surchargé pour comparer le texte plutôt que de s'assurer qu'elles référencent le même objet.

Pourquoi la chaîne n'est-elle pas simplement un type de valeur?

Davy8
la source
Étant donné que pour les types immuables, la distinction est principalement un détail d'implémentation (en laissant de iscôté les tests), la réponse est probablement "pour des raisons historiques". Les performances de copie ne peuvent pas être la raison, car il n'est pas nécessaire de copier physiquement des objets immuables. Il est maintenant impossible de changer sans casser le code qui utilise réellement des iscontrôles (ou des contraintes similaires).
Elazar
BTW c'est la même réponse pour C ++ (bien que la distinction entre les types valeur et référence ne soit pas explicite dans le langage), la décision de se std::stringcomporter comme une collection est une vieille erreur qui ne peut pas être corrigée maintenant.
Elazar

Réponses:

333

Les chaînes ne sont pas des types de valeur car elles peuvent être énormes et doivent être stockées sur le tas. Les types de valeurs sont (dans toutes les implémentations du CLR pour l'instant) stockés sur la pile. L'allocation de piles de chaînes casserait toutes sortes de choses: la pile n'est que de 1 Mo pour 32 bits et 4 Mo pour 64 bits, vous devez encadrer chaque chaîne, ce qui entraînerait une pénalité de copie, vous ne pourriez pas interner des chaînes et l'utilisation de la mémoire serait ballon, etc ...

(Modifier: Ajout d'une clarification sur le stockage de type valeur étant un détail d'implémentation, ce qui conduit à cette situation où nous avons un type avec une sémantique de valeur non héritée de System.ValueType. Merci Ben.)

codekaizen
la source
75
Je suis tatillonne ici, mais seulement parce que cela me donne l'occasion de créer un lien vers un article de blog pertinent à la question: les types de valeur ne sont pas nécessairement stockés sur la pile. C'est le plus souvent vrai dans ms.net, mais pas du tout spécifié par la spécification CLI. La principale différence entre les types de valeur et de référence est que les types de référence suivent la sémantique de copie par valeur. Voir blogs.msdn.com/ericlippert/archive/2009/04/27/… et blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn
8
@Qwertie: Stringn'est pas de taille variable. Lorsque vous y ajoutez, vous créez en fait un autre Stringobjet, en lui allouant une nouvelle mémoire.
codekaizen
5
Cela dit, une chaîne aurait pu, en théorie, être un type de valeur (une structure), mais la "valeur" n'aurait été rien d'autre qu'une référence à la chaîne. Les concepteurs de .NET ont naturellement décidé de supprimer les intermédiaires (la gestion des structures était inefficace dans .NET 1.0, et il était naturel de suivre Java, dans lequel les chaînes étaient déjà définies comme type de référence plutôt que primitif. De plus, si la chaîne était un type valeur puis le convertir en objet nécessiterait qu'il soit encadré, une inefficacité inutile).
Qwertie
7
@codekaizen Qwertie a raison, mais je pense que la formulation était déroutante. Une chaîne peut avoir une taille différente d'une autre chaîne et donc, contrairement à un vrai type de valeur, le compilateur ne pouvait pas savoir à l'avance combien d'espace allouer pour stocker la valeur de chaîne. Par exemple, an Int32vaut toujours 4 octets, donc le compilateur alloue 4 octets chaque fois que vous définissez une variable de chaîne. Quelle quantité de mémoire le compilateur doit-il allouer lorsqu'il rencontre une intvariable (s'il s'agissait d'un type de valeur)? Sachez que la valeur n'a pas encore été affectée à ce moment.
Kevin Brock
2
Désolé, une faute de frappe dans mon commentaire que je ne peux pas corriger maintenant; cela aurait dû être .... Par exemple, an Int32vaut toujours 4 octets, donc le compilateur alloue 4 octets chaque fois que vous définissez une intvariable. Quelle quantité de mémoire le compilateur doit-il allouer lorsqu'il rencontre une stringvariable (s'il s'agissait d'un type de valeur)? Sachez que la valeur n'a pas encore été affectée à ce moment.
Kevin Brock
57

Ce n'est pas un type de valeur car les performances (espace et temps!) Seraient terribles s'il s'agissait d'un type de valeur et sa valeur devait être copiée à chaque fois qu'elle était transmise et renvoyée par des méthodes, etc.

Il a une sémantique de valeur pour garder le monde sain d'esprit. Pouvez-vous imaginer à quel point il serait difficile de coder si

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

prêt bà être false? Imaginez à quel point le codage serait difficile pour n'importe quelle application.

Jason
la source
44
Java n'est pas connu pour être rude.
jason
3
@Matt: exactement. Lorsque je suis passé en C #, c'était un peu déroutant, car j'utilisais toujours (et fais parfois parfois) .equals (..) pour comparer les chaînes alors que mes coéquipiers utilisaient simplement "==". Je n'ai jamais compris pourquoi ils n'ont pas laissé le "==" pour comparer les références, bien que si vous pensez, 90% du temps, vous voudrez probablement comparer le contenu et non les références pour les chaînes.
Juri
7
@Juri: En fait, je pense qu'il n'est jamais souhaitable de vérifier les références, car parfois new String("foo");et un autre new String("foo")peut évaluer dans la même référence, ce qui n'est pas ce que vous attendez d'un newopérateur. (Ou pouvez-vous me dire un cas où je voudrais comparer les références?)
Michael
1
@Michael Eh bien, vous devez inclure une comparaison de référence dans toutes les comparaisons pour rattraper la comparaison avec null. Un autre bon endroit pour comparer des références avec des chaînes est lors de la comparaison plutôt que de la comparaison d'égalité. Deux chaînes équivalentes, lorsqu'elles sont comparées, doivent renvoyer 0. La vérification de ce cas prend cependant autant de temps que l'exécution de toute la comparaison, ce n'est donc pas un raccourci utile. La vérification ReferenceEquals(x, y)est un test rapide et vous pouvez retourner immédiatement 0, et lorsqu'il est mélangé avec votre test nul n'ajoute même plus de travail.
Jon Hanna
1
... avoir des chaînes comme un type de valeur de ce style plutôt que comme un type de classe signifierait que la valeur par défaut de a stringpourrait se comporter comme une chaîne vide (comme c'était le cas dans les systèmes pré -.net) plutôt que comme une référence nulle. En fait, ma propre préférence serait d'avoir un type de valeur Stringqui contiendrait un type de référence NullableString, le premier ayant une valeur par défaut équivalente à String.Emptyet le second ayant un défaut de nullet avec des règles spéciales de boxe / unboxing (telles que boxer un défaut- valeur NullableStringdonnerait une référence à String.Empty).
supercat
26

La distinction entre les types de référence et les types de valeur est fondamentalement un compromis de performance dans la conception du langage. Les types de référence ont des frais généraux sur la construction et la destruction et la collecte des ordures, car ils sont créés sur le tas. Les types de valeurs, d'autre part, ont des frais généraux sur les appels de méthode (si la taille des données est supérieure à un pointeur), car l'objet entier est copié plutôt qu'un simple pointeur. Étant donné que les chaînes peuvent être (et sont généralement) beaucoup plus grandes que la taille d'un pointeur, elles sont conçues comme types de référence. De plus, comme l'a souligné Servy, la taille d'un type de valeur doit être connue au moment de la compilation, ce qui n'est pas toujours le cas pour les chaînes.

La question de la mutabilité est une question distincte. Les types de référence et les types de valeur peuvent être mutables ou immuables. Les types de valeurs sont généralement immuables, car la sémantique des types de valeurs mutables peut prêter à confusion.

Les types de référence sont généralement modifiables, mais peuvent être conçus comme immuables si cela a du sens. Les chaînes sont définies comme immuables car elles permettent certaines optimisations. Par exemple, si le même littéral de chaîne se produit plusieurs fois dans le même programme (ce qui est assez courant), le compilateur peut réutiliser le même objet.

Alors pourquoi "==" est-il surchargé pour comparer les chaînes par texte? Parce que c'est la sémantique la plus utile. Si deux chaînes sont égales par du texte, elles peuvent ou non être la même référence d'objet en raison des optimisations. Donc, comparer des références est assez inutile, alors que comparer du texte est presque toujours ce que vous voulez.

De manière plus générale, Strings a ce qu'on appelle la sémantique des valeurs . Il s'agit d'un concept plus général que les types de valeur, qui est un détail d'implémentation spécifique à C #. Les types de valeur ont une sémantique de valeur, mais les types de référence peuvent également avoir une sémantique de valeur. Lorsqu'un type a une sémantique de valeur, vous ne pouvez pas vraiment dire si l'implémentation sous-jacente est un type de référence ou un type de valeur, vous pouvez donc considérer cela comme un détail d'implémentation.

JacquesB
la source
La distinction entre les types de valeur et les types de référence ne concerne pas vraiment les performances. Il s'agit de savoir si une variable contient un objet réel ou une référence à un objet. Une chaîne ne pourrait jamais être un type de valeur car la taille d'une chaîne est variable; il devrait être constant pour être un type de valeur; la performance n'a presque rien à voir avec cela. Les types de référence ne sont pas du tout chers à créer du tout.
Servy
2
@Sevy: La taille d'une chaîne est constante.
JacquesB
Parce qu'il contient simplement une référence à un tableau de caractères, qui est de taille variable. Avoir un type de valeur qui n'est que la «valeur» réelle était un type de référence serait d'autant plus déroutant, car il aurait toujours une sémantique de référence pour toutes les utilisations intensives.
Servy
1
@Sevy: La taille d'un tableau est constante.
JacquesB
1
Une fois que vous avez créé un tableau, sa taille est constante, mais tous les tableaux du monde entier n'ont pas tous exactement la même taille. C'est mon point. Pour qu'une chaîne soit un type de valeur, toutes les chaînes existantes doivent toutes avoir exactement la même taille, car c'est ainsi que les types de valeur sont conçus dans .NET. Il doit être en mesure de réserver de l'espace de stockage pour ces types de valeur avant d'avoir réellement une valeur , de sorte que la taille doit être connue au moment de la compilation . Un tel stringtype devrait avoir un tampon de caractères d'une certaine taille fixe, ce qui serait à la fois restrictif et très inefficace.
Servy
16

Il s'agit d'une réponse tardive à une vieille question, mais toutes les autres réponses ne sont pas pertinentes, à savoir que .NET n'avait pas de génériques avant .NET 2.0 en 2005.

Stringest un type de référence au lieu d'un type de valeur car il était d'une importance cruciale pour Microsoft de s'assurer que les chaînes pouvaient être stockées de la manière la plus efficace dans des collections non génériques , telles que System.Collections.ArrayList.

Le stockage d'un type de valeur dans une collection non générique nécessite une conversion spéciale vers le type objectappelé boxe. Lorsque le CLR met en boîte un type de valeur, il encapsule la valeur dans un System.Objectet le stocke sur le tas managé.

La lecture de la valeur de la collection nécessite l'opération inverse qui est appelée unboxing.

La boxe et le déballage ont un coût non négligeable: la boxe nécessite une allocation supplémentaire, le déballage nécessite une vérification de type.

Certaines réponses prétendent incorrectement qui stringn'auraient jamais pu être implémentées en tant que type de valeur car sa taille est variable. En fait, il est facile d'implémenter une chaîne en tant que structure de données de longueur fixe en utilisant une stratégie d'optimisation de petite chaîne: les chaînes seraient stockées en mémoire directement sous la forme d'une séquence de caractères Unicode, à l'exception des grandes chaînes qui seraient stockées en tant que pointeur vers un tampon externe. Les deux représentations peuvent être conçues pour avoir la même longueur fixe, c'est-à-dire la taille d'un pointeur.

Si les génériques avaient existé dès le premier jour, je suppose que la chaîne comme type de valeur aurait probablement été une meilleure solution, avec une sémantique plus simple, une meilleure utilisation de la mémoire et une meilleure localité de cache. Un List<string>contenant uniquement de petites chaînes aurait pu être un seul bloc de mémoire contigu.

ZunTzu
la source
Merci pour cette réponse! J'ai regardé toutes les autres réponses en disant des choses sur les allocations de tas et de piles, tandis que la pile est un détail d'implémentation . Après tout, stringne contient que sa taille et un pointeur vers le chartableau de toute façon, donc ce ne serait pas un "type de valeur énorme". Mais c'est une raison simple et pertinente pour cette décision de conception. Merci!
V0ldek
8

Non seulement les chaînes sont des types de référence immuables. Délégués multi-cast aussi. C'est pourquoi il est sûr d'écrire

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

Je suppose que les chaînes sont immuables car c'est la méthode la plus sûre pour travailler avec elles et allouer de la mémoire. Pourquoi ce ne sont pas des types de valeur? Les auteurs précédents ont raison sur la taille de la pile, etc. J'ajouterais également que faire des chaînes un type de référence permet d'économiser sur la taille de l'assemblage lorsque vous utilisez la même chaîne constante dans le programme. Si vous définissez

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

Il est probable que les deux instances de la constante "ma chaîne" ne seront allouées qu'une seule fois dans votre assembly.

Si vous souhaitez gérer des chaînes comme le type de référence habituel, placez la chaîne dans un nouveau StringBuilder (chaîne (s)). Ou utilisez MemoryStreams.

Si vous devez créer une bibliothèque, où vous vous attendez à ce qu'une énorme chaîne soit passée dans vos fonctions, définissez un paramètre en tant que StringBuilder ou en tant que Stream.

Bogdan_Ch
la source
1
Il existe de nombreux exemples de types de référence immuables. Et en ce qui concerne l'exemple de chaîne, qui est en effet à peu près garanti sous les implémentations actuelles - techniquement, c'est par module (pas par assemblage) - mais c'est presque toujours la même chose ...
Marc Gravell
5
Concernant le dernier point: StringBuilder ne vous aide pas si vous essayez de passer une grande chaîne (car il est en fait implémenté en tant que chaîne) - StringBuilder est utile pour manipuler une chaîne plusieurs fois.
Marc Gravell
Voulez-vous dire gestionnaire délégué, pas hadler? (désolé d'être pointilleux .. mais il est très proche d'un nom de famille (pas commun) que je connais ....)
Pure.Krome
6

En outre, la façon dont les chaînes sont implémentées (différentes pour chaque plate-forme) et lorsque vous commencez à les assembler. Comme utiliser un StringBuilder. Il alloue un tampon pour vous copier, une fois que vous atteignez la fin, il vous alloue encore plus de mémoire, dans l'espoir que si vous faites une grande concaténation, les performances ne seront pas entravées.

Peut-être que Jon Skeet peut aider ici?

Chris
la source
5

Il s'agit principalement d'un problème de performances.

Avoir des chaînes se comportent comme un type de valeur LIKE aide lors de l'écriture de code, mais le faire être un type de valeur ferait une énorme performance.

Pour un regard en profondeur, jetez un œil à un bel article sur les chaînes dans le framework .net.

Denis Troller
la source
3

En termes très simples, toute valeur ayant une taille définie peut être traitée comme un type de valeur.

saurav.net
la source
Cela devrait être un commentaire
ρяσѕρєя K
plus facile à comprendre pour ppl nouveau en c #
LONG
2

Comment savoir si stringc'est un type de référence? Je ne suis pas sûr que la façon dont il est mis en œuvre soit importante. Les chaînes en C # sont immuables précisément afin que vous n'ayez pas à vous soucier de ce problème.


la source
C'est un type de référence (je crois) car il ne dérive pas de System.ValueType De MSDN Remarques sur System.ValueType: Les types de données sont séparés en types de valeur et types de référence. Les types de valeurs sont alloués en pile ou alloués en ligne dans une structure. Les types de référence sont alloués en tas.
Davy8
Les types de référence et de valeur sont dérivés de l'objet de classe de base ultime. Dans les cas où il est nécessaire qu'un type de valeur se comporte comme un objet, un wrapper qui fait ressembler le type de valeur à un objet de référence est alloué sur le tas et la valeur du type de valeur y est copiée.
Davy8
L'encapsuleur est marqué afin que le système sache qu'il contient un type de valeur. Ce processus est appelé boxe, et le processus inverse est appelé unboxing. La boxe et le déballage permettent à tout type d'être traité comme un objet. (Dans le site postérieur, aurait probablement dû simplement être lié à l'article.)
Davy8
2

En fait, les chaînes ont très peu de ressemblances avec les types de valeur. Pour commencer, tous les types de valeurs ne sont pas immuables, vous pouvez modifier la valeur d'un Int32 à votre guise et ce serait toujours la même adresse sur la pile.

Les chaînes sont immuables pour une très bonne raison, cela n'a rien à voir avec le fait qu'il s'agit d'un type de référence, mais a beaucoup à voir avec la gestion de la mémoire. Il est juste plus efficace de créer un nouvel objet lorsque la taille de la chaîne change que de déplacer des éléments sur le tas géré. Je pense que vous mélangez ensemble des types valeur / référence et des concepts d'objets immuables.

Pour autant que "==": Comme vous l'avez dit, "==" est une surcharge d'opérateur, et encore une fois, il a été implémenté pour une très bonne raison pour rendre le framework plus utile lorsque vous travaillez avec des chaînes.

WebMatrix
la source
Je me rends compte que les types de valeur ne sont pas par définition immuables, mais la plupart des meilleures pratiques semblent suggérer qu'elles devraient l'être lors de la création de la vôtre. J'ai dit des caractéristiques, pas des propriétés des types de valeur, ce qui signifie pour moi que souvent les types de valeur en présentent, mais pas nécessairement par définition
Davy8
5
@WebMatrix, @ Davy8: Les types primitifs (int, double, bool, ...) sont immuables.
jason
1
@Jason, je pensais que le terme immuable s'applique principalement aux objets (types de référence) qui ne peuvent pas changer après l'initialisation, comme les chaînes lorsque la valeur des chaînes change, en interne une nouvelle instance d'une chaîne est créée et l'objet d'origine reste inchangé. Comment cela s'applique-t-il aux types de valeur?
WebMatrix
8
D'une manière ou d'une autre, dans "int n = 4; n = 9;", ce n'est pas que votre variable int soit "immuable", dans le sens de "constante"; c'est que la valeur 4 est immuable, elle ne change pas en 9. Votre variable int "n" a d'abord une valeur de 4 puis une valeur différente, 9; mais les valeurs elles-mêmes sont immuables. Franchement, pour moi, c'est très proche de wtf.
Daniel Daranas
1
+1. J'en ai assez d'entendre que "les chaînes sont comme des types de valeur" alors qu'elles ne le sont tout simplement pas.
Jon Hanna
1

N'est pas aussi simple que les chaînes sont composées de tableaux de caractères. Je regarde les chaînes comme des tableaux de caractères []. Par conséquent, ils se trouvent sur le tas car l'emplacement de mémoire de référence est stocké sur la pile et pointe vers le début de l'emplacement de mémoire du tableau sur le tas. La taille de la chaîne n'est pas connue avant qu'elle ne soit allouée ... parfait pour le tas.

C'est pourquoi une chaîne est vraiment immuable car lorsque vous la changez même si elle est de la même taille, le compilateur ne le sait pas et doit allouer un nouveau tableau et affecter des caractères aux positions dans le tableau. Cela a du sens si vous considérez les chaînes comme un moyen pour les langages de vous éviter d'allouer de la mémoire à la volée (lire la programmation en C)

BionicCyborg
la source
1
"La taille de la chaîne n'est pas connue avant qu'elle ne soit allouée" - ceci est incorrect dans le CLR.
codekaizen
-1

Au risque d'obtenir un autre mystérieux vote négatif ... le fait que beaucoup mentionnent la pile et la mémoire en ce qui concerne les types de valeur et les types primitifs est parce qu'ils doivent s'insérer dans un registre du microprocesseur. Vous ne pouvez pas pousser ou faire sauter quelque chose vers / depuis la pile si cela prend plus de bits qu'un registre ... Les instructions sont, par exemple, "pop eax" - parce que eax a une largeur de 32 bits sur un système 32 bits.

Les types primitifs à virgule flottante sont gérés par la FPU, qui a une largeur de 80 bits.

Tout cela a été décidé bien avant qu'il n'y ait un langage OOP pour masquer la définition du type primitif et je suppose que le type de valeur est un terme qui a été créé spécifiquement pour les langages OOP.

jinzai
la source