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

Una Cadena es un tipo de referencia a pesar de que 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?

Las cadenas no son tipos de valores, ya que pueden ser enormes y deben almacenarse en el montón. Los tipos de valor son (en todas las implementaciones del CLR hasta el momento) almacenados en la stack. Las cadenas de asignación de stack romperían todo tipo de cosas: la stack es solo de 1 MB para 32 bits y de 4 MB para 64 bits, debe encasillar cada cadena, incurrir en una penalización de copia, no puede internar cadenas y el uso de memoria volaría, etc …

(Editar: Se agregó una aclaración sobre el almacenamiento de tipo de valor que es un detalle de implementación, lo que lleva a esta situación en la que tenemos un tipo con valor sematics que no hereda de System.ValueType. Gracias Ben).

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 pasara a métodos retornados, etc.

Tiene una semántica de valores 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); 

establecer b para ser false ? Imagínese lo difícil que sería codificar casi cualquier aplicación.

La distinción entre tipos de referencia y tipos de valores es básicamente una compensación de rendimiento en el diseño del lenguaje. Los tipos de referencia tienen algunos gastos generales en construcción y destrucción y recolección de basura, porque se crean en el montón. Los tipos de valor, por otro lado, tienen una sobrecarga en las llamadas a métodos (si el tamaño de los datos es mayor que un puntero), ya que 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 tiempo de comstackción, que no siempre es el caso para las cadenas.

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

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

Entonces, ¿por qué está “==” sobrecargado para comparar cadenas por 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. Entonces, comparar referencias es bastante inútil, mientras que comparar texto es casi siempre lo que quieres.

Hablando en términos más generales, Strings tiene lo que se denomina semántica de valores . 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 valores tienen semántica de valores, pero los tipos de referencia también pueden tener semántica de valores. Cuando un tipo tiene una semántica de valor, no se puede decir realmente si la implementación subyacente es un tipo de referencia o un tipo de valor, por lo que se puede considerar un detalle de implementación.

No solo las cadenas son tipos de referencia inmutables. Delegados de reparto múltiple 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 ellos y asignar memoria. ¿Por qué no son tipos de valor? Los autores anteriores tienen razón sobre el tamaño de la stack, etc. También agregaría que hacer cadenas de tipos de referencia permite ahorrar en el tamaño del conjunto cuando se utiliza la misma cadena constante en el progtwig. Si defines

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

Lo más probable es que ambas instancias de la constante “mi cadena” 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 s). O usa MemoryStreams.

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

Esta es una respuesta tardía a una pregunta anterior, pero a todas las otras respuestas les falta el punto, que es que .NET no tenía generics hasta .NET 2.0 en 2005.

String es un tipo de referencia en lugar de un tipo de valor porque era de vital importancia para Microsoft asegurar que las cadenas se pudieran almacenar de la manera más eficiente en colecciones no genéricas , como System.Collection.ArrayList .

Almacenar un valor-tipo en una colección no genérica requiere una conversión especial al object tipo que se llama boxeo. Cuando el CLR incluye un tipo de valor, envuelve el valor dentro de un objeto System.Object y 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 desempaquetado tienen un costo no despreciable: el boxeo requiere una asignación adicional, el desempaquetado requiere verificación de tipo.

Algunas respuestas afirman incorrectamente que la string nunca podría 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 en la memoria directamente como una secuencia de caracteres Unicode, excepto para cadenas grandes que se almacenarían como punteros 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 generics hubieran existido desde el primer día, creo que tener cadena como un tipo de valor probablemente habría sido una mejor solución, con una semántica más simple, mejor uso de la memoria y una mejor localidad de memoria caché. Una List contiene solo cadenas pequeñas podría haber sido un único bloque contiguo de memoria.

Además, la forma en que se implementan las cadenas (diferente para cada plataforma) y cuando comienzas a unirlas. Como usar un StringBuilder . Se asigna un búfer para que pueda copiar, una vez que llega al final, asigna aún más memoria para usted, con la esperanza de que si lo hace un gran rendimiento de concatenación no se verá obstaculizado.

¿Tal vez Jon Skeet puede ayudarnos aquí?

Es principalmente un problema de rendimiento.

Tener cadenas se comporta como MEJOR tipo de valor ayuda al escribir código, pero tenerlo SER un tipo de valor haría un gran golpe de rendimiento.

Para un vistazo en profundidad, eche un vistazo a un buen artículo sobre cadenas en .NET Framework.

¿Cómo se puede decir que la string es 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.

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

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 gestión de la memoria. Es más eficiente crear un objeto nuevo cuando cambia el tamaño de la cadena que cambiar las cosas en el montón administrado. Creo que estás mezclando los tipos de valor / referencia y los conceptos de objetos inmutables.

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

No es tan simple como las cadenas están compuestas de 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 stack y apunta al comienzo de la ubicación de la memoria de la matriz en el montón. El tamaño de cadena no se conoce antes de asignarlo … perfecto para el montón.

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

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

A riesgo de obtener otro misterioso voto negativo … el hecho de que muchos mencionen la stack y la memoria con respecto a los tipos de valores y tipos primitivos es porque deben caber en un registro en el microprocesador. No puede empujar ni sacar algo de la stack si lleva 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 coma flotante son manejados por la FPU, que tiene 80 bits de ancho.

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