Tipo de referência de string C #?


163

Eu sei que "string" em c # é um tipo de referência. Isso está no MSDN. No entanto, esse código não funciona como deveria:

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";
    }
}

A saída deve ser "antes de passar" "depois de passar", pois estou passando a string como parâmetro e sendo um tipo de referência, a segunda instrução de saída deve reconhecer que o texto foi alterado no método TestI. No entanto, eu recebo "antes de passar" "antes de passar", fazendo parecer que é passado por valor e não por ref. Entendo que as strings são imutáveis, mas não vejo como isso explicaria o que está acontecendo aqui. o que estou perdendo? Obrigado.


Veja o artigo referido por Jon abaixo. O comportamento mencionado também pode ser reproduzido por ponteiros C ++.
Sesh

Muito boa explicação no MSDN também.
Dimi_Pel 02/04

Respostas:


211

A referência à string é passada por valor. Há uma grande diferença entre passar uma referência por valor e passar um objeto por referência. É lamentável que a palavra "referência" seja usada nos dois casos.

Se você fazer passar a referência seqüência de referência, ele vai funcionar como você 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";
    }
}

Agora você precisa distinguir entre fazer alterações no objeto ao qual uma referência se refere e fazer uma alteração em uma variável (como um parâmetro) para permitir que ela se refira a um objeto diferente. Não podemos fazer alterações em uma string porque elas são imutáveis, mas podemos demonstrá-la com um 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");
    }
}

Veja meu artigo sobre a passagem de parâmetros para mais detalhes.


2
Concordo, só quero deixar claro que o uso do modificador ref também funciona para tipos sem referência, ou seja, ambos são conceitos bem separados.
eglasius

2
@ Jon Skeet amou a nota lateral em seu artigo. Você deveria ter referencedisso como resposta
Nithish Inpursuit Ofhappiness

36

Se tivermos que responder à pergunta: String é um tipo de referência e se comporta como referência. Passamos um parâmetro que contém uma referência, e não a string real. O problema está na função:

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

O parâmetro testmantém uma referência à string, mas é uma cópia. Temos duas variáveis ​​apontando para a string. E como quaisquer operações com cadeias realmente criam um novo objeto, fazemos nossa cópia local para apontar para a nova cadeia. Mas a testvariável original não é alterada.

As soluções sugeridas para colocar refna declaração da função e na invocação funcionam porque não passaremos o valor da testvariável, mas passaremos apenas uma referência a ela. Portanto, quaisquer alterações dentro da função refletirão a variável original.

Quero repetir no final: String é um tipo de referência, mas como é imutável, a linha test = "after passing";realmente cria um novo objeto e nossa cópia da variável testé alterada para apontar para a nova string.


25

Como outros já declararam, o Stringtipo no .NET é imutável e sua referência é passada por valor.

No código original, assim que essa linha for executada:

test = "after passing";

então testnão está mais se referindo ao objeto original. Criamos um novo String objeto e atribuímos testa referência a esse objeto no heap gerenciado.

Eu sinto que muitas pessoas viajam até aqui, pois não há construtor formal visível para lembrá-las. Nesse caso, isso está acontecendo nos bastidores, pois o Stringtipo tem suporte de linguagem na forma como é construído.

Portanto, é por isso que a mudança para testnão é visível fora do escopo do TestI(string)método - passamos a referência por valor e agora esse valor mudou! Mas se a Stringreferência foi passada por referência, quando a referência mudou, a veremos fora do escopo do TestI(string)método.

A palavra - chave ref ou out é necessária neste caso. Acho que a outpalavra - chave pode ser um pouco mais adequada para essa situação específica.

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";
    }
}

ref = função externa inicializada, out = inicializada dentro da função ou em outras palavras; ref é bidirecional, out é somente fora. Portanto, certamente ref deve ser usado.
Paul Zahra

@PaulZahra: outprecisa ser atribuído dentro do método para a compilação do código. refnão tem esse requisito. Também outparâmetros são inicializados fora do método - o código nesta resposta é um contra-exemplo.
Derek W

Deve esclarecer - os outparâmetros podem ser inicializados fora do método, mas não precisam. Nesse caso, queremos inicializar o outparâmetro para demonstrar um ponto sobre a natureza do stringtipo no .NET.
Derek W

9

Na verdade, seria o mesmo para qualquer objeto, por exemplo, ser um tipo de referência e passar por referência são duas coisas diferentes em c #.

Isso funcionaria, mas isso se aplica independentemente do tipo:

public static void TestI(ref string test)

Também sobre string ser um tipo de referência, também é especial. Ele foi projetado para ser imutável, para que todos os seus métodos não modifiquem a instância (eles retornam um novo). Ele também possui algumas coisas extras para desempenho.


7

Aqui está uma boa maneira de pensar sobre a diferença entre tipos de valor, passagem por valor, tipos de referência e passagem por referência:

Uma variável é um contêiner.

Uma variável do tipo valor contém uma instância. Uma variável do tipo de referência contém um ponteiro para uma instância armazenada em outro local.

A modificação de uma variável do tipo valor muda a instância que ela contém. A modificação de uma variável do tipo de referência altera a instância para a qual ela aponta.

Variáveis ​​de tipo de referência separadas podem apontar para a mesma instância. Portanto, a mesma instância pode ser modificada por qualquer variável que aponte para ela.

Um argumento passado por valor é um novo contêiner com uma nova cópia do conteúdo. Um argumento passado por referência é o contêiner original com seu conteúdo original.

Quando um argumento do tipo valor é passado por valor: A reatribuição do conteúdo do argumento não tem efeito fora do escopo, porque o contêiner é exclusivo. A modificação do argumento não tem efeito fora do escopo, porque a instância é uma cópia independente.

Quando um argumento do tipo de referência é passado por valor: A reatribuição do conteúdo do argumento não tem efeito fora do escopo, porque o contêiner é exclusivo. A modificação do conteúdo do argumento afeta o escopo externo, porque o ponteiro copiado aponta para uma instância compartilhada.

Quando qualquer argumento é passado por referência: A reatribuição do conteúdo do argumento afeta o escopo externo, porque o contêiner é compartilhado. A modificação do conteúdo do argumento afeta o escopo externo, porque o conteúdo é compartilhado.

Em conclusão:

Uma variável de sequência é uma variável do tipo de referência. Portanto, ele contém um ponteiro para uma instância armazenada em outro local. Quando passado por valor, seu ponteiro é copiado, portanto, a modificação de um argumento de string deve afetar a instância compartilhada. No entanto, uma instância de string não possui propriedades mutáveis, portanto, um argumento de string não pode ser modificado de qualquer maneira. Quando passado por referência, o contêiner do ponteiro é compartilhado, portanto, a reatribuição ainda afetará o escopo externo.


6

" Uma imagem vale mais que mil palavras ".

Eu tenho um exemplo simples aqui, é semelhante ao seu caso.

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

Isso é o que aconteceu:

insira a descrição da imagem aqui

  • Linhas 1 e 2: s1e as s2variáveis ​​referenciam o mesmo "abc"objeto de string.
  • Linha 3: como as strings são imutáveis , o "abc"objeto string não se modifica (para "def"), mas um novo "def"objeto string é criado e, em seguida, faz s1referência a ele.
  • Linha 4: s2ainda faz referência ao "abc"objeto string, então essa é a saída.

5

As respostas acima são úteis, eu gostaria de adicionar um exemplo que acho que está demonstrando claramente o que acontece quando passamos o parâmetro sem a palavra-chave ref, mesmo quando esse parâmetro é um tipo de referência:

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.
        }

1
Essa explicação funcionou melhor para mim. Então, basicamente, passamos tudo por valor, apesar do fato de a variável em si ser do tipo valor ou referência, a menos que usemos a palavra-chave ref (ou out). Não é importante em nossa codificação diária, porque geralmente não definimos nossos objetos como nulos ou para uma instância diferente dentro de um método em que eles foram passados, em vez disso, definimos suas propriedades ou chamamos seus métodos. No caso de "string", configurá-lo para uma nova instância acontece o tempo todo, mas a novidade não é visível e isso dá uma interpretação falsa aos olhos não treinados. Corrija-me se estiver errado.
Ε Г И І И О

3

Para mentes curiosas e para concluir a conversa: Sim, String é um tipo de referência :

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

Mas observe que essa alteração funciona apenas em um bloco não seguro ! porque Strings são imutáveis (do MSDN):

O conteúdo de um objeto de sequência não pode ser alterado após a criação do objeto, embora a sintaxe faça com que pareça que você pode fazer isso. Por exemplo, quando você escreve esse código, o compilador realmente cria um novo objeto de seqüência de caracteres para armazenar a nova sequência de caracteres e esse novo objeto é atribuído a b. A sequência "h" é qualificada para a coleta de lixo.

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

E lembre-se de que:

Embora a sequência seja um tipo de referência, os operadores de igualdade ( ==e !=) são definidos para comparar os valores dos objetos da sequência, não as referências.


0

Acredito que seu código é análogo ao seguinte, e você não deveria esperar que o valor fosse alterado pelo mesmo motivo que não seria aqui:

 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
 }

-1

Experimentar:


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

3
Sua resposta deve conter mais do que apenas código. Também deve conter uma explicação sobre o porquê de funcionar.
Charles Caldwell
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.