¿Cómo se pasan las cadenas en .NET?

121

Cuando paso una stringa una función, ¿se pasa un puntero al contenido de la cadena o se pasa toda la cadena a la función en la pila como lo structharía a?

Cole Johnson
fuente

Respuestas:

278

Se pasa una referencia; sin embargo, técnicamente no se pasa por referencia. Ésta es una distinción sutil, pero muy importante. Considere el siguiente código:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Hay tres cosas que necesita saber para comprender lo que sucede aquí:

  1. Las cadenas son tipos de referencia en C #.
  2. También son inmutables, por lo que cada vez que haces algo que parece que estás cambiando la cuerda, no lo estás. Se crea una cadena completamente nueva, se apunta la referencia a ella y se desecha la anterior.
  3. Aunque las cadenas son tipos de referencia, strMainno se pasa por referencia. Es un tipo de referencia, pero la referencia en sí se pasa por valor . Cada vez que pasa un parámetro sin la refpalabra clave (sin contar los outparámetros), pasa algo por valor.

Eso debe significar que estás ... pasando una referencia por valor. Dado que es un tipo de referencia, solo se copió la referencia en la pila. Pero ¿qué significa eso?

Pasar tipos de referencia por valor: ya lo está haciendo

Las variables de C # son tipos de referencia o tipos de valor . Los parámetros de C # se pasan por referencia o por valor . La terminología es un problema aquí; estos suenan como lo mismo, pero no lo son.

Si pasa un parámetro de CUALQUIER tipo y no usa la refpalabra clave, entonces lo ha pasado por valor. Si lo pasó por valor, lo que realmente aprobó fue una copia. Pero si el parámetro era un tipo de referencia, entonces lo que copió fue la referencia, no lo que apuntaba.

Aquí está la primera línea del Mainmétodo:

string strMain = "main";

Hemos creado dos cosas en esta línea: una cadena con el valor mainalmacenado en la memoria en algún lugar, y una variable de referencia llamada strMainapuntando hacia ella.

DoSomething(strMain);

Ahora pasamos esa referencia a DoSomething. Lo pasamos por valor, lo que significa que hicimos una copia. Es un tipo de referencia, lo que significa que copiamos la referencia, no la cadena en sí. Ahora tenemos dos referencias que cada una apunta al mismo valor en la memoria.

Dentro del llamado

Aquí está la parte superior del DoSomethingmétodo:

void DoSomething(string strLocal)

Sin refpalabra clave, entonces strLocaly strMainson dos referencias diferentes que apuntan al mismo valor. Si reasignamos strLocal...

strLocal = "local";   

... no hemos cambiado el valor almacenado; Tomamos la referencia llamada strLocaly la apuntamos a una cuerda nueva. ¿Qué pasa con strMaincuando hacemos eso? Nada. Todavía apunta a la vieja cuerda.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Inmutabilidad

Cambiemos el escenario por un segundo. Imagina que no estamos trabajando con cadenas, sino con algún tipo de referencia mutable, como una clase que has creado.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Si sigue la referencia objLocalal objeto al que apunta, puede cambiar sus propiedades:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

Todavía hay solo uno MutableThingen la memoria, y tanto la referencia copiada como la referencia original todavía apuntan a él. Las propiedades del MutableThingmismo han cambiado :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

¡Ah, pero las cuerdas son inmutables! No hay ninguna ChangeMepropiedad que configurar. No puede hacer strLocal[3] = 'H'en C # como podría hacerlo con una charmatriz de estilo C ; tienes que construir una cadena completamente nueva en su lugar. La única forma de cambiar strLocales apuntar la referencia a otra cadena, y eso significa que nada de lo que haga strLocalpuede afectar strMain. El valor es inmutable y la referencia es una copia.

Pasando una referencia por referencia

Para demostrar que hay una diferencia, esto es lo que sucede cuando pasa una referencia por referencia:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Esta vez, la cadena en Mainrealmente se cambia porque pasó la referencia sin copiarla en la pila.

Entonces, aunque las cadenas son tipos de referencia, pasarlas por valor significa que lo que suceda en el destinatario de la llamada no afectará a la cadena en el llamador. Pero como son tipos de referencia, no es necesario que copie la cadena completa en la memoria cuando quiera pasarla.

Recursos adicionales:

Justin Morgan
fuente
3
@TheLight - Lo siento, pero se equivoca aquí cuando dice: "Un tipo de referencia se pasa por referencia de forma predeterminada". De forma predeterminada, todos los parámetros se pasan por valor, pero con los tipos de referencia, esto significa que la referencia se pasa por valor. Está combinando tipos de referencia con parámetros de referencia, lo cual es comprensible porque es una distinción muy confusa. Consulte la sección Pasar tipos de referencia por valor aquí. Su artículo vinculado es bastante correcto, pero en realidad apoya mi punto.
Justin Morgan
1
@JustinMorgan No quiero que aparezca un hilo de comentarios muerto, pero creo que el comentario de TheLight tiene sentido si piensas en C. En C, los datos son solo un bloque de memoria. Una referencia es un puntero a ese bloque de memoria. Si pasa todo el bloque de memoria a una función, eso se llama "pasar por valor". Si pasa el puntero, se llama "pasar por referencia". En C #, no existe la noción de pasar todo el bloque de memoria, por lo que redefinieron "pasar por valor" para significar pasar el puntero. Eso parece incorrecto, ¡pero un puntero es solo un bloque de memoria también! Para mí, la terminología es bastante arbitraria
rliu
@roliu - El problema es que no estamos trabajando en C, y C # es extremadamente diferente a pesar de su nombre y sintaxis similares. Por un lado, las referencias no son lo mismo que los punteros y pensar en ellos de esa manera puede conducir a dificultades. Sin embargo, el mayor problema es que "pasar por referencia" tiene un significado muy específico en C #, que requiere la refpalabra clave. Para demostrar que pasar por referencia marca la diferencia, vea esta demostración: rextester.com/WKBG5978
Justin Morgan
1
@JustinMorgan Estoy de acuerdo en que mezclar terminología de C y C # es malo, pero, aunque disfruté de la publicación de Lippert, no estoy de acuerdo en que pensar en las referencias como punteros empañe particularmente algo aquí. La publicación del blog describe cómo pensar en una referencia como un puntero le da demasiado poder. Soy consciente de que la refpalabra clave tiene utilidad, solo estaba tratando de explicar por qué uno podría pensar en pasar un tipo de referencia por valor en C # parece la noción "tradicional" (es decir, C) de pasar por referencia (y pasar un tipo de referencia por referencia en C # parece más como pasar una referencia a una referencia por valor).
rliu
2
Estás en lo cierto, pero creo que se refería a la forma en @roliu una función, como Foo(string bar)podría ser pensado como Foo(char* bar)mientras que Foo(ref string bar)sería Foo(char** bar)(o Foo(char*& bar), o Foo(string& bar)en C ++). Claro, no es como deberías pensarlo todos los días, pero en realidad me ayudó a comprender finalmente lo que está sucediendo bajo el capó.
Cole Johnson
23

Las cadenas en C # son objetos de referencia inmutables. Esto significa que las referencias a ellos se pasan (por valor) y, una vez que se crea una cadena, no se puede modificar. Los métodos que producen versiones modificadas de la cadena (subcadenas, versiones recortadas, etc.) crean copias modificadas de la cadena original.

dasblinkenlight
fuente
10

Las cadenas son casos especiales. Cada instancia es inmutable. Cuando cambia el valor de una cadena, está asignando una nueva cadena en la memoria.

Entonces, solo se pasa la referencia a su función, pero cuando se edita la cadena, se convierte en una nueva instancia y no modifica la instancia anterior.

Enigmatividad
fuente
4
Las cadenas no son un caso especial en este aspecto. Es muy fácil crear objetos inmutables que podrían tener la misma semántica. (Es decir, una instancia de un tipo que no expone un método para mutarlo ...)
Las cadenas son casos especiales: son efectivamente tipos de referencia inmutables que parecen ser mutables en el sentido de que se comportan como tipos de valor.
Enigmativity
1
@Enigmativity Por esa lógica, entonces Uri(clase) y Guid(estructura) también son casos especiales. No veo cómo System.Stringactúa como un "tipo de valor" más que otros tipos inmutables ... de orígenes de clase o estructura.
3
@pst: las cadenas tienen una semántica de creación especial, a diferencia de Uri& Guid, solo puede asignar un valor literal de cadena a una variable de cadena. La cadena parece ser mutable, como una intreasignación, pero está creando un objeto implícitamente, sin newpalabra clave.
Enigmativity
3
String es un caso especial, pero no tiene relevancia para esta pregunta. El tipo de valor, el tipo de referencia, cualquier tipo actuará de la misma manera en esta pregunta.
Kirk Broadhurst