C # tipo de referencia de cadena?

163

Sé que "cadena" en C # es un tipo de referencia. Esto está en MSDN. Sin embargo, este código no funciona como debería entonces:

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

La salida debe ser "antes de pasar" "después de pasar" ya que paso la cadena como parámetro y es un tipo de referencia, la segunda declaración de salida debe reconocer que el texto cambió en el método TestI. Sin embargo, obtengo "antes de pasar" "antes de pasar" haciendo que parezca que se pasa por valor, no por ref. Entiendo que las cadenas son inmutables, pero no veo cómo eso explicaría lo que está sucediendo aquí. ¿Qué me estoy perdiendo? Gracias.


fuente
Vea el artículo referido por Jon a continuación. El comportamiento que menciona también puede reproducirse mediante punteros de C ++.
Sesh
Muy buena explicación en MSDN también.
Dimi_Pel

Respuestas:

211

La referencia a la cadena se pasa por valor. Hay una gran diferencia entre pasar una referencia por valor y pasar un objeto por referencia. Es lamentable que la palabra "referencia" se use en ambos casos.

Si se hace pasar la cadena de referencia por referencia, que funcionará como se espera:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

Ahora debe distinguir entre hacer cambios en el objeto al que se refiere una referencia y hacer un cambio en una variable (como un parámetro) para permitir que se refiera a un objeto diferente. No podemos realizar cambios en una cadena porque las cadenas son inmutables, pero podemos demostrarlo con un StringBuilder:

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

Vea mi artículo sobre paso de parámetros para más detalles.

Jon Skeet
fuente
2
De acuerdo, solo quiero dejar en claro que el uso del modificador de referencia también funciona para tipos sin referencia, es decir, ambos son conceptos bastante separados.
eglasius
2
@Jon Skeet le encantó la nota al margen de su artículo. Deberías tener referencedeso como respuesta
Nithish Inpursuit Ofhappiness
36

Si tenemos que responder la pregunta: String es un tipo de referencia y se comporta como una referencia. Pasamos un parámetro que contiene una referencia, no la cadena real. El problema está en la función:

public static void TestI(string test)
{
    test = "after passing";
}

El parámetro testcontiene una referencia a la cadena pero es una copia. Tenemos dos variables apuntando a la cadena. Y debido a que cualquier operación con cadenas realmente crea un nuevo objeto, hacemos nuestra copia local para apuntar a la nueva cadena. Pero la testvariable original no cambia.

Las soluciones sugeridas para poner refen la declaración de función y en la invocación funcionan porque no pasaremos el valor de la testvariable sino que le pasaremos solo una referencia. Por lo tanto, cualquier cambio dentro de la función reflejará la variable original.

Quiero repetir al final: la cadena es un tipo de referencia, pero como es inmutable, la línea test = "after passing";realmente crea un nuevo objeto y nuestra copia de la variable testse cambia para que apunte a la nueva cadena.

Martin Dimitrov
fuente
25

Como han dicho otros, el Stringtipo en .NET es inmutable y su referencia se pasa por valor.

En el código original, tan pronto como se ejecute esta línea:

test = "after passing";

entonces testya no se refiere al objeto original. Creamos un nuevo String objeto y lo asignamos testpara hacer referencia a ese objeto en el montón administrado.

Siento que muchas personas se tropiezan aquí ya que no hay un constructor formal visible para recordarles. En este caso, está sucediendo detrás de escena ya que el Stringtipo tiene soporte de idioma en cómo se construye.

Por lo tanto, es por eso que el cambio testno es visible fuera del alcance del TestI(string)método - hemos pasado la referencia por su valor y ahora que el valor ha cambiado! Pero si la Stringreferencia se pasó por referencia, entonces cuando la referencia haya cambiado la veremos fuera del alcance del TestI(string)método.

En este caso, se necesita la palabra clave ref o out . Creo que la outpalabra clave podría ser un poco más adecuada para esta situación particular.

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}
Derek W
fuente
ref = inicializada fuera de la función, out = inicializada dentro de la función, o en otras palabras; ref es bidireccional, out es solo out. Por lo tanto, seguramente se debe usar ref.
Paul Zahra
@PaulZahra: outdebe asignarse dentro del método para que el código se compile. refNo tiene tal requisito. Además, los outparámetros se inicializan fuera del método: el código de esta respuesta es un contraejemplo.
Derek W
Debería aclarar: los outparámetros se pueden inicializar fuera del método, pero no es necesario. En este caso, queremos inicializar el outparámetro para demostrar un punto sobre la naturaleza del stringtipo en .NET.
Derek W
9

En realidad, hubiera sido lo mismo para cualquier objeto, es decir, ser un tipo de referencia y pasar por referencia son 2 cosas diferentes en c #.

Esto funcionaría, pero eso se aplica independientemente del tipo:

public static void TestI(ref string test)

También acerca de que la cadena es un tipo de referencia, también es especial. Está diseñado para ser inmutable, por lo que todos sus métodos no modificarán la instancia (devuelven una nueva). También tiene algunas cosas adicionales para el rendimiento.

eglasius
fuente
7

Aquí hay una buena manera de pensar sobre la diferencia entre los tipos de valor, el paso por valor, los tipos de referencia y el paso por referencia:

Una variable es un contenedor.

Una variable de tipo de valor contiene una instancia. Una variable de tipo de referencia contiene un puntero a una instancia almacenada en otro lugar.

La modificación de una variable de tipo de valor muta la instancia que contiene. La modificación de una variable de tipo de referencia muta la instancia a la que apunta.

Las variables de tipo de referencia separadas pueden apuntar a la misma instancia. Por lo tanto, la misma instancia puede ser mutada a través de cualquier variable que la señale.

Un argumento pasado por valor es un nuevo contenedor con una nueva copia del contenido. Un argumento pasado por referencia es el contenedor original con su contenido original.

Cuando un argumento de tipo valor se pasa por valor: la reasignación del contenido del argumento no tiene ningún efecto fuera del alcance, porque el contenedor es único. Modificar el argumento no tiene ningún efecto fuera del alcance, porque la instancia es una copia independiente.

Cuando un argumento de tipo de referencia se pasa por valor: la reasignación del contenido del argumento no tiene ningún efecto fuera del alcance, porque el contenedor es único. La modificación del contenido del argumento afecta el alcance externo, porque el puntero copiado apunta a una instancia compartida.

Cuando se pasa un argumento por referencia: la reasignación del contenido del argumento afecta el alcance externo, porque el contenedor se comparte. La modificación del contenido del argumento afecta el alcance externo, porque el contenido se comparte.

En conclusión:

Una variable de cadena es una variable de tipo de referencia. Por lo tanto, contiene un puntero a una instancia almacenada en otro lugar. Cuando se pasa por valor, su puntero se copia, por lo que modificar un argumento de cadena debería afectar la instancia compartida. Sin embargo, una instancia de cadena no tiene propiedades mutables, por lo que un argumento de cadena no se puede modificar de todos modos. Cuando se pasa por referencia, el contenedor del puntero se comparte, por lo que la reasignación seguirá afectando el alcance externo.

Bryan
fuente
6

" Una imagen vale más que mil palabras ".

Tengo un ejemplo simple aquí, es similar a su caso.

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

Esto es lo que pasó:

ingrese la descripción de la imagen aquí

  • Línea 1 y 2: s1y las s2variables hacen referencia al mismo "abc"objeto de cadena.
  • Línea 3: debido a que las cadenas son inmutables , el "abc"objeto de cadena no se modifica a sí mismo (a "def"), sino "def"que se crea un nuevo objeto de cadena y luego hace s1referencia a él.
  • Línea 4: s2todavía se hace referencia al "abc"objeto de cadena, por lo que ese es el resultado
Messi
fuente
5

Las respuestas anteriores son útiles, solo me gustaría agregar un ejemplo que creo que demuestra claramente lo que sucede cuando pasamos el parámetro sin la palabra clave ref, incluso cuando ese parámetro es un tipo de referencia:

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }
BornToCode
fuente
1
Esta explicación me funcionó mejor. Básicamente, pasamos todo por valor a pesar del hecho de que la variable en sí misma es valor o tipo de referencia a menos que usemos la palabra clave ref (o out). No es prominente con nuestra codificación diaria porque generalmente no establecemos nuestros objetos en nulos o en una instancia diferente dentro de un método donde se pasaron, sino que establecemos sus propiedades o llamamos a sus métodos. En el caso de "string", establecerlo en una nueva instancia ocurre todo el tiempo, pero la actualización no es visible y eso da una interpretación falsa al ojo inexperto. Corrígeme si está equivocado.
Ε Г И І И О
3

Para mentes curiosas y para completar la conversación: Sí, String es un tipo de referencia :

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

¡Pero tenga en cuenta que este cambio solo funciona en un bloque inseguro ! porque las cadenas son inmutables (de MSDN):

El contenido de un objeto de cadena no se puede cambiar después de crear el objeto, aunque la sintaxis hace que parezca que puede hacerlo. Por ejemplo, cuando escribe este código, el compilador crea un nuevo objeto de cadena para contener la nueva secuencia de caracteres, y ese nuevo objeto se asigna a b. La cadena "h" es elegible para la recolección de basura.

string b = "h";  
b += "ello";  

Y tenga en cuenta que:

Aunque la cadena es un tipo de referencia, los operadores de igualdad ( ==y !=) están definidos para comparar los valores de los objetos de cadena, no las referencias.

Nueva York
fuente
0

Creo que su código es análogo al siguiente, y no debería haber esperado que el valor haya cambiado por la misma razón que no lo haría aquí:

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }
Dave Cousineau
fuente
-1

Tratar:


public static void TestI(ref string test)
    {
        test = "after passing";
    }
Marius Kjeldahl
fuente
3
Su respuesta debe contener más que solo código. También debe contener una explicación de por qué funciona.
Charles Caldwell