En C #, ¿por qué String es un tipo de referencia que se comporta como un tipo de valor?

371

Una cadena es un tipo de referencia, aunque tiene la mayoría de las características de un tipo de valor, como ser inmutable y tener == sobrecargado para comparar el texto en lugar de asegurarse de que hacen referencia al mismo objeto.

¿Por qué la cadena no es solo un tipo de valor entonces?

Davy8
fuente
Dado que para los tipos inmutables la distinción es principalmente un detalle de implementación (dejando de islado las pruebas), la respuesta es probablemente "por razones históricas". El rendimiento de la copia no puede ser la razón, ya que no es necesario copiar físicamente objetos inmutables. Ahora es imposible cambiar sin romper el código que realmente usa ischeques (o restricciones similares).
Elazar
Por cierto, esta es la misma respuesta para C ++ (aunque la distinción entre el valor y los tipos de referencia no es explícita en el lenguaje), la decisión de std::stringcomportarse como una colección es un antiguo error que no se puede solucionar ahora.
Elazar

Respuestas:

333

Las cadenas no son tipos de valor, ya que pueden ser enormes y deben almacenarse en el montón. Los tipos de valor se almacenan en la pila (en todas las implementaciones de CLR hasta el momento). La asignación de cadenas de la pila rompería todo tipo de cosas: la pila es de solo 1 MB para 32 bits y 4 MB para 64 bits, tendría que encuadrar cada cadena, incurriendo en una penalización de copia, no podría internar cadenas y uso de memoria globo, etc ...

(Editar: se agregó una aclaración sobre el almacenamiento de tipos de valor como un detalle de implementación, lo que lleva a esta situación en la que tenemos un tipo con valores semánticos que no heredan de System.ValueType. Gracias Ben).

codekaizen
fuente
75
Me estoy burlando de aquí, pero solo porque me da la oportunidad de vincular a una publicación de blog relevante para la pregunta: los tipos de valor no se almacenan necesariamente en la pila. Es más frecuente en ms.net, pero no está especificado en absoluto por la especificación CLI. La principal diferencia entre los tipos de valor y referencia es que los tipos de referencia siguen una semántica de copia por valor. Ver blogs.msdn.com/ericlippert/archive/2009/04/27/… y blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn
8
@Qwertie: Stringno es de tamaño variable. Cuando le agrega, en realidad está creando otro Stringobjeto, asignándole nueva memoria.
codekaizen
55
Dicho esto, una cadena podría, en teoría, haber sido un tipo de valor (una estructura), pero el "valor" no habría sido más que una referencia a la cadena. Los diseñadores de .NET naturalmente decidieron eliminar al intermediario (el manejo de la estructura era ineficiente en .NET 1.0, y era natural seguir a Java, en el que las cadenas ya estaban definidas como un tipo de referencia, en lugar de primitivo. Además, si la cadena fuera un tipo de valor y luego convertirlo en objeto requeriría que esté encuadrado, una ineficiencia innecesaria).
Qwertie
77
@codekaizen Qwertie tiene razón, pero creo que la redacción fue confusa. Una cadena puede tener un tamaño diferente que otra cadena y, por lo tanto, a diferencia de un tipo de valor verdadero, el compilador no puede saber de antemano cuánto espacio asignar para almacenar el valor de la cadena. Por ejemplo, an Int32siempre es de 4 bytes, por lo tanto, el compilador asigna 4 bytes cada vez que define una variable de cadena. ¿Cuánta memoria debe asignar el compilador cuando encuentra una intvariable (si fuera un tipo de valor)? Comprenda que el valor aún no se ha asignado en ese momento.
Kevin Brock
2
Lo siento, un error tipográfico en mi comentario que no puedo solucionar ahora; eso debería haber sido ... Por ejemplo, un Int32siempre es de 4 bytes, por lo tanto, el compilador asigna 4 bytes cada vez que define una intvariable. ¿Cuánta memoria debe asignar el compilador cuando encuentra una stringvariable (si fuera un tipo de valor)? Comprenda que el valor aún no se ha asignado en ese momento.
Kevin Brock
57

No es un tipo de valor porque el rendimiento (¡espacio y tiempo!) Sería terrible si fuera un tipo de valor y su valor tuviera que copiarse cada vez que se pasa y se devuelve desde métodos, etc.

Tiene una semántica de valor para mantener al mundo cuerdo. ¿Te imaginas lo difícil que sería codificar si

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

listo bpara ser false? Imagine lo difícil que sería codificar casi cualquier aplicación.

jason
fuente
44
Java no es conocido por ser concisa.
jason
3
@ Matt: exactamente. Cuando cambié a C #, esto fue un poco confuso, ya que siempre usaba (y todavía lo hago a veces) .equals (..) para comparar cadenas mientras mis compañeros de equipo solo usaban "==". Nunca entendí por qué no dejaron el "==" para comparar las referencias, aunque si piensas, el 90% de las veces probablemente querrás comparar el contenido, no las referencias de las cadenas.
Juri
77
@Juri: En realidad, creo que nunca es deseable verificar las referencias, ya que a veces new String("foo");y otras new String("foo")pueden evaluar en la misma referencia, qué tipo de no es lo que esperaría que hiciera un newoperador. (¿O puede decirme un caso en el que me gustaría comparar las referencias?)
Michael
1
@Michael Bueno, debes incluir una comparación de referencia en todas las comparaciones para atrapar la comparación con nulo. Otro buen lugar para comparar referencias con cadenas es cuando se compara en lugar de la comparación de igualdad. Dos cadenas equivalentes, cuando se comparan, deberían devolver 0. La comprobación de este caso, aunque de todos modos lleva tanto tiempo como la ejecución de toda la comparación, por lo que no es un atajo útil. Verificar ReferenceEquals(x, y)es una prueba rápida y puede devolver 0 inmediatamente, y cuando se mezcla con su prueba nula ni siquiera agrega más trabajo.
Jon Hanna
1
... que las cadenas sean un tipo de valor de ese estilo en lugar de ser un tipo de clase significaría que el valor predeterminado de a stringpodría comportarse como una cadena vacía (como lo era en los sistemas pre.net) en lugar de como una referencia nula. En realidad, mi preferencia sería tener un tipo de valor Stringque contuviera un tipo de referencia NullableString, con el primero con un valor predeterminado equivalente String.Emptyy el segundo con un valor predeterminado null, y con reglas especiales de boxing / unboxing (como boxing un default- valorado NullableStringdaría una referencia a String.Empty).
supercat
26

La distinción entre los tipos de referencia y los tipos de valor son básicamente una compensación de rendimiento en el diseño del lenguaje. Los tipos de referencia tienen algunos gastos generales en la construcción y la destrucción y la recolección de basura, ya que se crean en el montón. Por otro lado, los tipos de valor tienen una sobrecarga en las llamadas a métodos (si el tamaño de los datos es mayor que un puntero), porque todo el objeto se copia en lugar de solo un puntero. Debido a que las cadenas pueden ser (y típicamente son) mucho más grandes que el tamaño de un puntero, están diseñadas como tipos de referencia. Además, como señaló Servy, el tamaño de un tipo de valor debe conocerse en el momento de la compilación, que no siempre es el caso de las cadenas.

La cuestión de la mutabilidad es un tema aparte. Tanto los tipos de referencia como los tipos de valor pueden ser mutables o inmutables. Sin embargo, los tipos de valor suelen ser inmutables, ya que la semántica para los tipos de valor mutable puede ser confusa.

Los tipos de referencia son generalmente mutables, pero pueden diseñarse como inmutables si tiene sentido. Las cadenas se definen como inmutables porque hacen posibles ciertas optimizaciones. Por ejemplo, si el mismo literal de cadena aparece varias veces en el mismo programa (que es bastante común), el compilador puede reutilizar el mismo objeto.

Entonces, ¿por qué se sobrecarga "==" para comparar cadenas de texto? Porque es la semántica más útil. Si dos cadenas son iguales por texto, pueden o no ser la misma referencia de objeto debido a las optimizaciones. Por lo tanto, comparar referencias es bastante inútil, mientras que comparar texto es casi siempre lo que desea.

Hablando de manera más general, Strings tiene lo que se denomina semántica de valor . Este es un concepto más general que los tipos de valor, que es un detalle de implementación específico de C #. Los tipos de valor tienen semántica de valor, pero los tipos de referencia también pueden tener semántica de valor. Cuando un tipo tiene semántica de valor, realmente no se puede saber si la implementación subyacente es un tipo de referencia o un tipo de valor, por lo que puede considerar que es un detalle de implementación.

JacquesB
fuente
La distinción entre los tipos de valor y los tipos de referencia no se trata realmente del rendimiento. Se trata de si una variable contiene un objeto real o una referencia a un objeto. Una cadena nunca podría ser un tipo de valor porque el tamaño de una cadena es variable; necesitaría ser constante para ser un tipo de valor; el rendimiento no tiene casi nada que ver con eso. Los tipos de referencia tampoco son caros de crear.
Servy
2
@Sevy: el tamaño de una cadena es constante.
JacquesB
Porque solo contiene una referencia a una matriz de caracteres, que es de tamaño variable. Tener un tipo de valor cuyo único "valor" real fuera un tipo de referencia sería aún más confuso, ya que todavía tendría semántica de referencia para todos los propósitos intensivos.
Servy
1
@Sevy: el tamaño de una matriz es constante.
JacquesB
1
Una vez que haya creado una matriz, su tamaño es constante, pero no todas las matrices del mundo entero tienen exactamente el mismo tamaño. Ese es mi punto. Para que una cadena sea un tipo de valor, todas las cadenas existentes deberían tener exactamente el mismo tamaño, porque así es como se diseñan los tipos de valor en .NET. Debe poder reservar espacio de almacenamiento para tales tipos de valor antes de tener un valor real , por lo que el tamaño debe conocerse en el momento de la compilación . Tal stringtipo necesitaría tener un búfer de caracteres de algún tamaño fijo, lo cual sería restrictivo y altamente ineficiente.
Servicio
16

Esta es una respuesta tardía a una pregunta anterior, pero a todas las demás respuestas les falta el punto, que es que .NET no tenía genéricos hasta .NET 2.0 en 2005.

Stringes un tipo de referencia en lugar de un tipo de valor porque era de crucial importancia para Microsoft garantizar que las cadenas se pudieran almacenar de la manera más eficiente en colecciones no genéricas , como System.Collections.ArrayList.

El almacenamiento de un tipo de valor en una colección no genérica requiere una conversión especial al tipo objectque se llama boxeo. Cuando CLR encuadra un tipo de valor, envuelve el valor dentro de System.Objectay lo almacena en el montón administrado.

Leer el valor de la colección requiere la operación inversa que se llama unboxing.

Tanto el boxeo como el unboxing tienen un costo no despreciable: el boxeo requiere una asignación adicional, el unboxing requiere una verificación de tipo.

Algunas respuestas afirman incorrectamente que string nunca podrían haberse implementado como un tipo de valor porque su tamaño es variable. En realidad, es fácil implementar cadenas como una estructura de datos de longitud fija utilizando una estrategia de optimización de cadenas pequeñas: las cadenas se almacenarían directamente en la memoria como una secuencia de caracteres Unicode, excepto las cadenas grandes que se almacenarían como un puntero a un búfer externo. Ambas representaciones pueden diseñarse para tener la misma longitud fija, es decir, el tamaño de un puntero.

Si los genéricos hubieran existido desde el primer día, supongo que tener una cadena como tipo de valor probablemente hubiera sido una mejor solución, con una semántica más simple, un mejor uso de la memoria y una mejor localidad de caché. Una que List<string>contenga solo cadenas pequeñas podría haber sido un solo bloque contiguo de memoria.

ZunTzu
fuente
¡Gracias por esta respuesta! He estado mirando todas las otras respuestas que dicen cosas sobre el montón y las asignaciones de pila, mientras que la pila es un detalle de implementación . Después de todo, stringcontiene solo su tamaño y un puntero a la charmatriz de todos modos, por lo que no sería un "tipo de gran valor". Pero esta es una razón simple y relevante para esta decisión de diseño. ¡Gracias!
V0ldek
8

No solo las cadenas son tipos de referencia inmutables. Delegados multi-elenco también. Por eso es seguro escribir

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

Supongo que las cadenas son inmutables porque este es el método más seguro para trabajar con ellas y asignar memoria. ¿Por qué no son tipos de valor? Los autores anteriores tienen razón sobre el tamaño de la pila, etc. También agregaría que hacer que las cadenas sean tipos de referencia permiten ahorrar en el tamaño del ensamblaje cuando se usa la misma cadena constante en el programa. Si usted define

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

Lo más probable es que ambas instancias de la constante "my string" se asignen en su ensamblaje solo una vez.

Si desea administrar cadenas como el tipo de referencia habitual, coloque la cadena dentro de un nuevo StringBuilder (cadena). O use MemoryStreams.

Si va a crear una biblioteca, donde espera que se pasen cadenas enormes en sus funciones, defina un parámetro como StringBuilder o Stream.

Bogdan_Ch
fuente
1
Hay muchos ejemplos de tipos de referencia inmutables. Y en el ejemplo de la cadena, eso está prácticamente garantizado por las implementaciones actuales, técnicamente es por módulo (no por ensamblaje), pero eso es casi siempre lo mismo ...
Marc Gravell
55
Re el último punto: StringBuilder no ayuda si intenta pasar una cadena grande (ya que de todos modos se implementa como una cadena) - StringBuilder es útil para manipular una cadena varias veces.
Marc Gravell
¿Querías decir delegado manejador, no hadler? (perdón por ser exigente ... pero está muy cerca de un apellido (no común) que sé ...)
Pure.Krome
6

Además, la forma en que se implementan las cadenas (diferentes para cada plataforma) y cuándo comienza a unirlas. Como usar unStringBuilder . Le asigna un búfer para que copie, una vez que llegue al final, le asigna aún más memoria, con la esperanza de que si realiza una gran concatenación no se verá obstaculizado.

¿Quizás Jon Skeet pueda ayudar aquí?

Chris
fuente
5

Es principalmente un problema de rendimiento.

Hacer que las cadenas se comporten como el tipo de valor LIKE ayuda al escribir código, pero tenerlo como un tipo de valor supondría un gran impacto en el rendimiento.

Para una mirada en profundidad, eche un vistazo a un buen artículo sobre cadenas en el marco .net.

Denis Troller
fuente
3

En palabras muy simples, cualquier valor que tenga un tamaño definido puede tratarse como un tipo de valor.

saurav.net
fuente
Esto debería ser un comentario
ρяσѕρєя K
más fácil de entender para personas nuevas en c #
LARGO
2

¿Cómo puedes saber si stringes un tipo de referencia? No estoy seguro de que importe cómo se implementa. Las cadenas en C # son inmutables precisamente para que no tenga que preocuparse por este problema.


fuente
Es un tipo de referencia (creo) porque no se deriva de System.ValueType de MSDN Observaciones en System.ValueType: los tipos de datos se separan en tipos de valores y tipos de referencia. Los tipos de valor se asignan en pila o se asignan en línea en una estructura. Los tipos de referencia están asignados en el montón.
Davy8
Tanto los tipos de referencia como los de valor se derivan del último objeto de clase base. En los casos en que es necesario que un tipo de valor se comporte como un objeto, se asigna un contenedor que hace que el tipo de valor parezca un objeto de referencia en el montón, y el valor del tipo de valor se copia en él.
Davy8
El contenedor está marcado para que el sistema sepa que contiene un tipo de valor. Este proceso se conoce como boxeo, y el proceso inverso se conoce como unboxing. El boxeo y el desempaquetado permiten que cualquier tipo sea tratado como un objeto. (En el sitio posterior, probablemente debería haber vinculado el artículo).
Davy8 8 de
2

En realidad, las cadenas tienen muy pocas semejanzas con los tipos de valor. Para empezar, no todos los tipos de valores son inmutables, puede cambiar el valor de un Int32 todo lo que quiera y seguiría siendo la misma dirección en la pila.

Las cadenas son inmutables por una muy buena razón, no tiene nada que ver con que sea un tipo de referencia, pero tiene mucho que ver con la administración de memoria. Es más eficiente crear un nuevo objeto cuando cambia el tamaño de la cadena que cambiar las cosas en el montón administrado. Creo que estás mezclando tipos de valor / referencia y conceptos de objetos inmutables.

En cuanto a "==": Como dijiste, "==" es una sobrecarga del operador, y nuevamente se implementó por una muy buena razón para hacer que el marco sea más útil cuando se trabaja con cadenas.

WebMatrix
fuente
Me doy cuenta de que los tipos de valor no son, por definición, inmutables, pero la mayoría de las mejores prácticas parecen sugerir que deberían serlo al crear el suyo. Dije características, no propiedades de los tipos de valor, lo que para mí significa que a menudo los tipos de valor exhiben estos, pero no necesariamente por definición
Davy8
55
@WebMatrix, @ Davy8: Los tipos primitivos (int, double, bool, ...) son inmutables.
Jason
1
@ Jason, pensé que el término inmutable se aplica principalmente a los objetos (tipos de referencia) que no pueden cambiar después de la inicialización, como las cadenas cuando cambia el valor de las cadenas, internamente se crea una nueva instancia de una cadena y el objeto original permanece sin cambios. ¿Cómo se aplica esto a los tipos de valor?
WebMatrix
8
De alguna manera, en "int n = 4; n = 9;", no es que su variable int sea "inmutable", en el sentido de "constante"; es que el valor 4 es inmutable, no cambia a 9. Su variable int "n" primero tiene un valor de 4 y luego un valor diferente, 9; pero los valores en sí mismos son inmutables. Francamente, para mí esto está muy cerca de wtf.
Daniel Daranas
1
+1. Estoy harto de escuchar que estas "cadenas son como tipos de valores" cuando simplemente no lo son.
Jon Hanna
1

No es tan simple como las cadenas están formadas por matrices de caracteres. Miro las cadenas como matrices de caracteres []. Por lo tanto, están en el montón porque la ubicación de la memoria de referencia se almacena en la pila y apunta al comienzo de la ubicación de la memoria de la matriz en el montón. El tamaño de la cadena no se conoce antes de que se asigne ... perfecto para el montón.

Es por eso que una cadena es realmente inmutable porque cuando la cambia, incluso si es del mismo tamaño, el compilador no lo sabe y tiene que asignar una nueva matriz y asignar caracteres a las posiciones en la matriz. Tiene sentido si piensas en las cadenas como una forma en que los lenguajes te protegen de tener que asignar memoria sobre la marcha (lee C como programación)

BionicCyborg
fuente
1
"el tamaño de la cadena no se conoce antes de que se asigne": esto es incorrecto en el CLR.
codekaizen
-1

A riesgo de obtener otro voto negativo misterioso ... el hecho de que muchos mencionen la pila y la memoria con respecto a los tipos de valor y los tipos primitivos se debe a que deben caber en un registro en el microprocesador. No puede empujar o hacer estallar algo hacia / desde la pila si toma más bits de los que tiene un registro ... las instrucciones son, por ejemplo, "pop eax", porque eax tiene 32 bits de ancho en un sistema de 32 bits.

Los tipos primitivos de punto flotante son manejados por la FPU, que tiene 80 bits de ancho.

Todo esto se decidió mucho antes de que hubiera un lenguaje OOP para ofuscar la definición de tipo primitivo y supongo que tipo de valor es un término que se ha creado específicamente para los lenguajes OOP.

jinzai
fuente